0

[fuchsia] Add support for synthesized/non-physical keypresses.

Handles input events in which there is a KeyMeaning (w/Unicode
codepoint) but no physical key (e.g. accented characters unsupported
by the physical key layout)

* Minor refactoring of KeyboardClient.
* Adds new browsertests.
* Substantial cleanups of redundant code in browsertests.


Bug: 1215319
Test: web_engine_browsertests
Change-Id: I863cf24e0db9bb19ae23c1db450c991b651b6d8b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2964484
Reviewed-by: Sharon Yang <yangsharon@chromium.org>
Commit-Queue: Kevin Marshall <kmarshall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#895710}
This commit is contained in:
Kevin Marshall
2021-06-24 18:49:19 +00:00
committed by Chromium LUCI CQ
parent 296d788a4a
commit 09837c8183
3 changed files with 249 additions and 108 deletions
fuchsia/engine/browser
ui/base/ime/fuchsia

@ -21,6 +21,8 @@
using fuchsia::input::Key;
using fuchsia::ui::input3::KeyEvent;
using fuchsia::ui::input3::KeyEventType;
using fuchsia::ui::input3::KeyMeaning;
using fuchsia::ui::input3::NonPrintableKey;
namespace {
@ -29,7 +31,7 @@ const char kKeyPress[] = "keypress";
const char kKeyUp[] = "keyup";
const char kKeyDicts[] = "keyDicts";
KeyEvent FakeKeyEvent(Key key, KeyEventType event_type) {
KeyEvent CreateKeyEvent(Key key, KeyEventType event_type) {
KeyEvent key_event;
key_event.set_timestamp(base::TimeTicks::Now().ToZxTime());
key_event.set_type(event_type);
@ -37,17 +39,40 @@ KeyEvent FakeKeyEvent(Key key, KeyEventType event_type) {
return key_event;
}
std::unique_ptr<base::Value> ExpectedKeyValue(base::StringPiece code,
base::StringPiece key,
base::StringPiece type) {
std::unique_ptr<base::Value> expected =
std::make_unique<base::DictionaryValue>();
expected->SetStringKey("code", code);
expected->SetStringKey("key", key);
expected->SetStringKey("type", type);
KeyEvent CreateCharacterEvent(uint32_t codepoint, KeyEventType event_type) {
KeyEvent key_event;
fuchsia::ui::input3::KeyMeaning meaning;
meaning.set_codepoint(codepoint);
key_event.set_key_meaning(std::move(meaning));
key_event.set_type(event_type);
key_event.set_timestamp(base::TimeTicks::Now().ToZxTime());
return key_event;
}
base::Value ExpectedKeyValue(base::StringPiece code,
base::StringPiece key,
base::StringPiece type) {
base::Value expected(base::Value::Type::DICTIONARY);
expected.SetStringKey("code", code);
expected.SetStringKey("key", key);
expected.SetStringKey("type", type);
return expected;
}
// Recursive base case.
template <typename T>
void AppendValueList(std::vector<T>* vec) {}
// Use tail recursion to emplace a sequence of Values into |vec|.
// It is used as an alternative to initializer lists, which don't work with
// move-only types like base::Value.
template <typename T, typename... Args>
void AppendValueList(std::vector<T>* vec, T&& value, Args&&... args) {
vec->push_back(std::move(value));
AppendValueList(vec, std::forward<base::Value>(args)...);
}
class FakeKeyboard : public fuchsia::ui::input3::testing::Keyboard_TestBase {
public:
explicit FakeKeyboard(sys::OutgoingDirectory* additional_services) {
@ -137,6 +162,18 @@ class InputTest : public cr_fuchsia::WebEngineBrowserTest {
context_impl()->GetFrameImplForTest(frame_ptr)->web_contents());
}
template <typename... Args>
void ExpectKeyEventsEqual(Args... events) {
std::vector<base::Value> expected;
AppendValueList(&expected, std::forward<Args>(events)...);
frame_for_test_.navigation_listener().RunUntilTitleEquals(
base::NumberToString(expected.size()));
absl::optional<base::Value> actual =
cr_fuchsia::ExecuteJavaScript(frame_for_test_.ptr().get(), kKeyDicts);
EXPECT_EQ(*actual, base::Value(expected));
}
// Used to publish fake services.
absl::optional<base::TestComponentContextForProcess> component_context_;
@ -145,99 +182,139 @@ class InputTest : public cr_fuchsia::WebEngineBrowserTest {
absl::optional<FakeKeyboard> keyboard_service_;
};
// Check that regular character keys are sent and received correctly.
IN_PROC_BROWSER_TEST_F(InputTest, CharacterKeys) {
const int kExpectedCharacterEventCount = 6;
// Check that printable keys are sent and received correctly.
IN_PROC_BROWSER_TEST_F(InputTest, PrintableKeys) {
// Send key press events from the Fuchsia keyboard service.
// Pressing character keys will generate a JavaScript keydown event followed
// by a keypress event. Releasing any key generates a keyup event.
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::A, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::KEY_8, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::KEY_8, KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::A, KeyEventType::RELEASED));
ExpectKeyEventsEqual(ExpectedKeyValue("KeyA", "a", kKeyDown),
ExpectedKeyValue("KeyA", "a", kKeyPress),
ExpectedKeyValue("Digit8", "8", kKeyDown),
ExpectedKeyValue("Digit8", "8", kKeyPress),
ExpectedKeyValue("Digit8", "8", kKeyUp),
ExpectedKeyValue("KeyA", "a", kKeyUp));
}
// Check that character virtual keys are sent and received correctly.
IN_PROC_BROWSER_TEST_F(InputTest, Characters) {
// Send key press events from the Fuchsia keyboard service.
// Pressing character keys will generate a JavaScript keydown event followed
// by a keypress event. Releasing any key generates a keyup event.
keyboard_service_->SendKeyEvent(
CreateCharacterEvent('A', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterEvent('A', KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateCharacterEvent('b', KeyEventType::PRESSED));
ExpectKeyEventsEqual(ExpectedKeyValue("", "A", kKeyPress),
ExpectedKeyValue("", "b", kKeyPress));
}
// Verify that character events are not affected by active modifiers.
IN_PROC_BROWSER_TEST_F(InputTest, ShiftCharacter) {
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::LEFT_SHIFT, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterEvent('a', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterEvent('a', KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::LEFT_SHIFT, KeyEventType::RELEASED));
ExpectKeyEventsEqual(
ExpectedKeyValue("ShiftLeft", "Shift", kKeyDown),
ExpectedKeyValue("", "a", kKeyPress), // Remains lowercase.
ExpectedKeyValue("ShiftLeft", "Shift", kKeyUp));
}
// Verifies that codepoints outside the 16-bit Unicode BMP are rejected.
IN_PROC_BROWSER_TEST_F(InputTest, CharacterInBmp) {
const wchar_t kSigma = 0x03C3;
keyboard_service_->SendKeyEvent(
CreateCharacterEvent(kSigma, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterEvent(kSigma, KeyEventType::RELEASED));
std::string expected_utf8;
ASSERT_TRUE(base::WideToUTF8(&kSigma, 1, &expected_utf8));
ExpectKeyEventsEqual(ExpectedKeyValue("", expected_utf8, kKeyPress));
}
// Verifies that codepoints beyond the range of allowable UCS-2 values
// are rejected.
IN_PROC_BROWSER_TEST_F(InputTest, CharacterBeyondBmp) {
const uint32_t kRamenEmoji = 0x1F35C;
// Send key press events from the Fuchsia keyboard service.
// Pressing character keys will generate a JavaScript keydown event followed
// by a keypress event. Releasing any key generates a keyup event.
keyboard_service_->SendKeyEvent(FakeKeyEvent(Key::A, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::KEY_8, KeyEventType::PRESSED));
CreateCharacterEvent(kRamenEmoji, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::KEY_8, KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(FakeKeyEvent(Key::A, KeyEventType::RELEASED));
frame_for_test_.navigation_listener().RunUntilTitleEquals(
base::NumberToString(kExpectedCharacterEventCount));
CreateCharacterEvent(kRamenEmoji, KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateCharacterEvent('a', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterEvent('a', KeyEventType::RELEASED));
absl::optional<base::Value> result =
cr_fuchsia::ExecuteJavaScript(frame_for_test_.ptr().get(), kKeyDicts);
base::ListValue expected;
expected.Set(0, ExpectedKeyValue("KeyA", "a", kKeyDown));
expected.Set(1, ExpectedKeyValue("KeyA", "a", kKeyPress));
expected.Set(2, ExpectedKeyValue("Digit8", "8", kKeyDown));
expected.Set(3, ExpectedKeyValue("Digit8", "8", kKeyPress));
expected.Set(4, ExpectedKeyValue("Digit8", "8", kKeyUp));
expected.Set(5, ExpectedKeyValue("KeyA", "a", kKeyUp));
EXPECT_EQ(*result, expected);
ExpectKeyEventsEqual(ExpectedKeyValue("", "a", kKeyPress));
}
IN_PROC_BROWSER_TEST_F(InputTest, ShiftCharacterKeys) {
const int kExpectedShiftCharacterEventCount = 10;
IN_PROC_BROWSER_TEST_F(InputTest, ShiftPrintableKeys) {
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::LEFT_SHIFT, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(FakeKeyEvent(Key::B, KeyEventType::PRESSED));
CreateKeyEvent(Key::LEFT_SHIFT, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::KEY_3, KeyEventType::PRESSED));
CreateKeyEvent(Key::B, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::SPACE, KeyEventType::PRESSED));
CreateKeyEvent(Key::KEY_1, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::LEFT_SHIFT, KeyEventType::RELEASED));
CreateKeyEvent(Key::SPACE, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::DOT, KeyEventType::PRESSED));
frame_for_test_.navigation_listener().RunUntilTitleEquals(
base::NumberToString(kExpectedShiftCharacterEventCount));
CreateKeyEvent(Key::LEFT_SHIFT, KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::DOT, KeyEventType::PRESSED));
// Note that non-character keys (e.g. shift, control) only generate key down
// and key up web events. They do not generate key pressed events.
absl::optional<base::Value> result =
cr_fuchsia::ExecuteJavaScript(frame_for_test_.ptr().get(), kKeyDicts);
base::ListValue expected;
expected.Set(0, ExpectedKeyValue("ShiftLeft", "Shift", kKeyDown));
expected.Set(1, ExpectedKeyValue("KeyB", "B", kKeyDown));
expected.Set(2, ExpectedKeyValue("KeyB", "B", kKeyPress));
expected.Set(3, ExpectedKeyValue("Digit3", "#", kKeyDown));
expected.Set(4, ExpectedKeyValue("Digit3", "#", kKeyPress));
expected.Set(5, ExpectedKeyValue("Space", " ", kKeyDown));
expected.Set(6, ExpectedKeyValue("Space", " ", kKeyPress));
expected.Set(7, ExpectedKeyValue("ShiftLeft", "Shift", kKeyUp));
expected.Set(8, ExpectedKeyValue("Period", ".", kKeyDown));
expected.Set(9, ExpectedKeyValue("Period", ".", kKeyPress));
EXPECT_EQ(*result, expected);
ExpectKeyEventsEqual(ExpectedKeyValue("ShiftLeft", "Shift", kKeyDown),
ExpectedKeyValue("KeyB", "B", kKeyDown),
ExpectedKeyValue("KeyB", "B", kKeyPress),
ExpectedKeyValue("Digit1", "!", kKeyDown),
ExpectedKeyValue("Digit1", "!", kKeyPress),
ExpectedKeyValue("Space", " ", kKeyDown),
ExpectedKeyValue("Space", " ", kKeyPress),
ExpectedKeyValue("ShiftLeft", "Shift", kKeyUp),
ExpectedKeyValue("Period", ".", kKeyDown),
ExpectedKeyValue("Period", ".", kKeyPress));
}
IN_PROC_BROWSER_TEST_F(InputTest, ShiftNonCharacterKeys) {
const int kExpectedShiftNonCharacterEventCount = 5;
IN_PROC_BROWSER_TEST_F(InputTest, ShiftNonPrintableKeys) {
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::RIGHT_SHIFT, KeyEventType::PRESSED));
CreateKeyEvent(Key::RIGHT_SHIFT, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::ENTER, KeyEventType::PRESSED));
CreateKeyEvent(Key::ENTER, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::LEFT_CTRL, KeyEventType::PRESSED));
CreateKeyEvent(Key::LEFT_CTRL, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
FakeKeyEvent(Key::RIGHT_SHIFT, KeyEventType::RELEASED));
frame_for_test_.navigation_listener().RunUntilTitleEquals(
base::NumberToString(kExpectedShiftNonCharacterEventCount));
CreateKeyEvent(Key::RIGHT_SHIFT, KeyEventType::RELEASED));
// Note that non-character keys (e.g. shift, control) only generate key down
// and key up web events. They do not generate key pressed events.
absl::optional<base::Value> result =
cr_fuchsia::ExecuteJavaScript(frame_for_test_.ptr().get(), kKeyDicts);
base::ListValue expected;
expected.Set(0, ExpectedKeyValue("ShiftRight", "Shift", kKeyDown));
expected.Set(1, ExpectedKeyValue("Enter", "Enter", kKeyDown));
expected.Set(2, ExpectedKeyValue("Enter", "Enter", kKeyPress));
expected.Set(3, ExpectedKeyValue("ControlLeft", "Control", kKeyDown));
expected.Set(4, ExpectedKeyValue("ShiftRight", "Shift", kKeyUp));
EXPECT_EQ(*result, expected);
ExpectKeyEventsEqual(ExpectedKeyValue("ShiftRight", "Shift", kKeyDown),
ExpectedKeyValue("Enter", "Enter", kKeyDown),
ExpectedKeyValue("Enter", "Enter", kKeyPress),
ExpectedKeyValue("ControlLeft", "Control", kKeyDown),
ExpectedKeyValue("ShiftRight", "Shift", kKeyUp));
}
IN_PROC_BROWSER_TEST_F(InputTest, Disconnect) {
@ -251,4 +328,4 @@ IN_PROC_BROWSER_TEST_F(InputTest, Disconnect) {
->GetBool());
}
} // namespace
} // namespace

@ -8,8 +8,10 @@
#include "base/logging.h"
#include "base/notreached.h"
#include "ui/events/fuchsia/input_event_sink.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/keycodes/keyboard_code_conversion_fuchsia.h"
namespace ui {
@ -34,6 +36,52 @@ int ModifiersToEventFlags(const fuchsia::ui::input3::Modifiers& modifiers) {
return event_flags;
}
absl::optional<EventType> ConvertKeyEventType(
fuchsia::ui::input3::KeyEventType type) {
switch (type) {
case fuchsia::ui::input3::KeyEventType::PRESSED:
return ET_KEY_PRESSED;
break;
case fuchsia::ui::input3::KeyEventType::RELEASED:
return ET_KEY_RELEASED;
break;
case fuchsia::ui::input3::KeyEventType::SYNC:
case fuchsia::ui::input3::KeyEventType::CANCEL:
// SYNC and CANCEL should not generate ui::Events.
return absl::nullopt;
default:
NOTREACHED() << "Unknown KeyEventType received: "
<< static_cast<int>(type);
return absl::nullopt;
}
}
// Creates an event for an event which has no |key|.
absl::optional<ui::KeyEvent> ConvertToCharacterEvent(
const fuchsia::ui::input3::KeyEvent& key_event) {
DCHECK(!key_event.has_key());
absl::optional<EventType> event_type = ConvertKeyEventType(key_event.type());
if (!event_type) {
return absl::nullopt;
}
if (event_type != ET_KEY_PRESSED) {
// Keypress phase cannot be tracked on keypresses without hardware keys,
// so only handle the "pressed" edge transition.
return absl::nullopt;
}
const uint32_t codepoint = key_event.key_meaning().codepoint();
if (codepoint > std::numeric_limits<char16_t>::max()) {
// TODO(crbug.com/1220260): Handle codepoints outside the BMP.
return absl::nullopt;
}
return ui::KeyEvent(*event_type, VKEY_UNKNOWN, DomCode::NONE,
EF_IS_SYNTHESIZED, DomKey::FromCharacter(codepoint),
base::TimeTicks::FromZxTime(key_event.timestamp()), true);
}
} // namespace
KeyboardClient::KeyboardClient(fuchsia::ui::input3::Keyboard* keyboard_service,
@ -58,6 +106,11 @@ KeyboardClient::~KeyboardClient() = default;
void KeyboardClient::OnKeyEvent(
fuchsia::ui::input3::KeyEvent key_event,
fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) {
if (!IsValid(key_event)) {
binding_.Close(ZX_ERR_INVALID_ARGS);
return;
}
if (ProcessKeyEvent(key_event)) {
callback(fuchsia::ui::input3::KeyEventStatus::HANDLED);
} else {
@ -65,45 +118,52 @@ void KeyboardClient::OnKeyEvent(
}
}
bool KeyboardClient::IsValid(const fuchsia::ui::input3::KeyEvent& key_event) {
if (!key_event.has_type() || !key_event.has_timestamp())
return false;
if (!key_event.has_key() && !key_event.has_key_meaning())
return false;
return true;
}
bool KeyboardClient::ProcessKeyEvent(
const fuchsia::ui::input3::KeyEvent& key_event) {
if (!key_event.has_type() || !key_event.has_key() ||
!key_event.has_timestamp()) {
LOG(ERROR) << "Could not process incomplete input3::KeyEvent.";
const bool generate_character_event = !key_event.has_key();
absl::optional<ui::KeyEvent> converted_event;
if (generate_character_event) {
converted_event = ConvertToCharacterEvent(key_event);
} else {
UpdateCachedModifiers(key_event);
converted_event = ConvertKeystrokeEvent(key_event);
}
if (!converted_event) {
return false;
}
// Update activation flags of modifier keys (SHIFT, ALT, etc). This needs to
// be done for all key event types.
UpdatedCachedModifiers(key_event);
event_sink_->DispatchEvent(&converted_event.value());
return converted_event->handled();
}
EventType event_type;
switch (key_event.type()) {
case fuchsia::ui::input3::KeyEventType::PRESSED:
event_type = ET_KEY_PRESSED;
break;
case fuchsia::ui::input3::KeyEventType::RELEASED:
event_type = ET_KEY_RELEASED;
break;
case fuchsia::ui::input3::KeyEventType::SYNC:
case fuchsia::ui::input3::KeyEventType::CANCEL:
// SYNC and CANCEL should not generate ui::Events.
return true;
default:
NOTIMPLEMENTED() << "Unknown KeyEventType received: "
<< static_cast<int>(event_type);
return false;
absl::optional<ui::KeyEvent> KeyboardClient::ConvertKeystrokeEvent(
const fuchsia::ui::input3::KeyEvent& key_event) {
DCHECK(key_event.has_key());
absl::optional<EventType> event_type = ConvertKeyEventType(key_event.type());
if (!event_type) {
return absl::nullopt;
}
// Convert |key_event| to a ui::KeyEvent.
DomCode dom_code =
KeycodeConverter::UsbKeycodeToDomCode(static_cast<int>(key_event.key()));
int event_flags = EventFlagsForCachedModifiers();
if (key_event.has_modifiers())
event_flags |= ModifiersToEventFlags(key_event.modifiers());
// TODO(https://crbug.com/1187257): Use input3.KeyMeaning instead of US layout
// as the default.
DomCode dom_code =
KeycodeConverter::UsbKeycodeToDomCode(static_cast<int>(key_event.key()));
DomKey dom_key;
KeyboardCode key_code;
if (!DomCodeToUsLayoutDomKey(dom_code, event_flags, &dom_key, &key_code)) {
@ -111,16 +171,13 @@ bool KeyboardClient::ProcessKeyEvent(
<< static_cast<uint32_t>(key_event.key());
}
ui::KeyEvent ui_key_event(event_type, key_code, dom_code, event_flags,
dom_key,
base::TimeTicks::FromZxTime(key_event.timestamp()));
event_sink_->DispatchEvent(&ui_key_event);
return ui_key_event.handled();
return ui::KeyEvent(*event_type, key_code, dom_code, event_flags, dom_key,
base::TimeTicks::FromZxTime(key_event.timestamp()));
}
// TODO(https://crbug.com/850697): Add additional modifiers as they become
// supported.
void KeyboardClient::UpdatedCachedModifiers(
void KeyboardClient::UpdateCachedModifiers(
const fuchsia::ui::input3::KeyEvent& key_event) {
// A SYNC event indicates that the key was pressed while the view gained input
// focus. A CANCEL event indicates the key was held when the view lost input

@ -9,6 +9,7 @@
#include <lib/fidl/cpp/binding.h>
#include "base/component_export.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/events/event.h"
namespace ui {
@ -34,6 +35,12 @@ class COMPONENT_EXPORT(UI_BASE_IME_FUCHSIA) KeyboardClient
fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) final;
private:
bool IsValid(const fuchsia::ui::input3::KeyEvent& key_event);
// Returns an unset value if the |key_event| type is unsupported.
absl::optional<ui::KeyEvent> ConvertKeystrokeEvent(
const fuchsia::ui::input3::KeyEvent& key_event);
// Handles converting and propagating |key_event|. Returns false if critical
// information about |key_event| is missing, or if the key's event type is not
// supported.
@ -42,7 +49,7 @@ class COMPONENT_EXPORT(UI_BASE_IME_FUCHSIA) KeyboardClient
bool ProcessKeyEvent(const fuchsia::ui::input3::KeyEvent& key_event);
// Update the value of modifiers such as shift.
void UpdatedCachedModifiers(const fuchsia::ui::input3::KeyEvent& key_event);
void UpdateCachedModifiers(const fuchsia::ui::input3::KeyEvent& key_event);
// Translate state of locally tracked modifier keys (e.g. shift, alt) into
// ui::Event flags.