LibWeb: Allow Option-generated text insertion
Track whether a keydown should perform text insertion separately from the delivered code point. Native frontends and text-oriented test paths can now mark events that came from text input, while shortcut-style key events keep Alt-modified default insertion suppressed. This lets macOS Option-generated text such as Option+A and Option+Space insert into editable controls without making plain Alt shortcuts insert their base character. Add coverage for Alt text, Ctrl+Alt text, separator text, Ctrl-only shortcuts, and Alt shortcuts.
This commit is contained in:
parent
6eb1afbf4b
commit
d9d8c5ce89
16 changed files with 107 additions and 31 deletions
|
|
@ -245,7 +245,7 @@ void EventLoop::process_input_events() const
|
|||
[&](KeyEvent const& key_event) {
|
||||
switch (key_event.type) {
|
||||
case KeyEvent::Type::KeyDown:
|
||||
return page.handle_keydown(key_event.key, key_event.modifiers, key_event.code_point, key_event.repeat);
|
||||
return page.handle_keydown(key_event.key, key_event.modifiers, key_event.code_point, key_event.repeat, key_event.should_insert_text);
|
||||
case KeyEvent::Type::KeyUp:
|
||||
return page.handle_keyup(key_event.key, key_event.modifiers, key_event.code_point, key_event.repeat);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -294,9 +294,9 @@ void Internals::send_text(HTML::HTMLElement& target, String const& text, WebIDL:
|
|||
|
||||
for (auto code_point : text.code_points()) {
|
||||
if (auto data = webdriver_key_to_key_code(code_point); data.has_value())
|
||||
page.handle_keydown(data->key_code, modifiers | data->additional_modifiers, data->code_point_to_send, false);
|
||||
page.handle_keydown(data->key_code, modifiers | data->additional_modifiers, data->code_point_to_send, false, data->code_point_to_send != 0);
|
||||
else
|
||||
page.handle_keydown(UIEvents::code_point_to_key_code(code_point), modifiers, code_point, false);
|
||||
page.handle_keydown(UIEvents::code_point_to_key_code(code_point), modifiers, code_point, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -305,7 +305,7 @@ void Internals::send_key(HTML::HTMLElement& target, String const& key_name, WebI
|
|||
auto key_code = UIEvents::key_code_from_string(key_name);
|
||||
target.focus();
|
||||
|
||||
page().handle_keydown(key_code, modifiers, 0, false);
|
||||
page().handle_keydown(key_code, modifiers, 0, false, false);
|
||||
}
|
||||
|
||||
void Internals::paste(HTML::HTMLElement& target, Utf16String const& text)
|
||||
|
|
@ -318,7 +318,7 @@ void Internals::paste(HTML::HTMLElement& target, Utf16String const& text)
|
|||
|
||||
void Internals::commit_text()
|
||||
{
|
||||
page().handle_keydown(UIEvents::Key_Return, 0, 0x0d, false);
|
||||
page().handle_keydown(UIEvents::Key_Return, 0, 0x0d, false, true);
|
||||
}
|
||||
|
||||
UIEvents::MouseButton Internals::button_from_unsigned_short(WebIDL::UnsignedShort button)
|
||||
|
|
|
|||
|
|
@ -869,16 +869,24 @@ static constexpr bool is_enter_key_or_interoperable_enter_key_combo(UIEvents::Ke
|
|||
return false;
|
||||
}
|
||||
|
||||
static constexpr bool should_ignore_keydown_event(u32 code_point, u32 modifiers)
|
||||
static bool should_ignore_keydown_event(u32 code_point, u32 modifiers, bool should_insert_text)
|
||||
{
|
||||
if (modifiers & (UIEvents::KeyModifier::Mod_Ctrl | UIEvents::KeyModifier::Mod_Alt | UIEvents::KeyModifier::Mod_Super))
|
||||
if (code_point == 0 || code_point == 27)
|
||||
return true;
|
||||
|
||||
// FIXME: There are probably also keys with non-zero code points that should be filtered out.
|
||||
return code_point == 0 || code_point == 27;
|
||||
if (modifiers & UIEvents::KeyModifier::Mod_Super)
|
||||
return true;
|
||||
|
||||
if ((modifiers & UIEvents::KeyModifier::Mod_Ctrl) && !(modifiers & UIEvents::KeyModifier::Mod_Alt))
|
||||
return true;
|
||||
|
||||
if ((modifiers & UIEvents::KeyModifier::Mod_Alt) && !should_insert_text)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u32 code_point, bool repeat)
|
||||
EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u32 code_point, bool repeat, bool should_insert_text)
|
||||
{
|
||||
if (!m_navigable->active_document())
|
||||
return EventResult::Dropped;
|
||||
|
|
@ -1047,7 +1055,7 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u
|
|||
}
|
||||
|
||||
// FIXME: Text editing shortcut keys (copy/paste etc.) should be handled here.
|
||||
if (!should_ignore_keydown_event(code_point, modifiers)) {
|
||||
if (!should_ignore_keydown_event(code_point, modifiers, should_insert_text)) {
|
||||
FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::insertText, m_navigable, code_point));
|
||||
target->handle_insert(UIEvents::InputTypes::insertText, Utf16String::from_code_point(code_point));
|
||||
return EventResult::Handled;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public:
|
|||
void update_hover_after_scroll();
|
||||
GC::Ptr<DOM::Node> target_node_for_mouse_position(CSSPixelPoint);
|
||||
|
||||
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat, bool should_insert_text);
|
||||
EventResult handle_keyup(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
|
||||
EventResult handle_drag_and_drop_event(DragEvent::Type, CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ namespace Web {
|
|||
|
||||
KeyEvent KeyEvent::clone_without_browser_data() const
|
||||
{
|
||||
return { type, key, modifiers, code_point, repeat, nullptr };
|
||||
return { type, key, modifiers, code_point, repeat, should_insert_text, nullptr };
|
||||
}
|
||||
|
||||
MouseEvent MouseEvent::clone_without_browser_data() const
|
||||
|
|
@ -36,6 +36,7 @@ ErrorOr<void> IPC::encode(Encoder& encoder, Web::KeyEvent const& event)
|
|||
TRY(encoder.encode(event.modifiers));
|
||||
TRY(encoder.encode(event.code_point));
|
||||
TRY(encoder.encode(event.repeat));
|
||||
TRY(encoder.encode(event.should_insert_text));
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
@ -47,8 +48,9 @@ ErrorOr<Web::KeyEvent> IPC::decode(Decoder& decoder)
|
|||
auto modifiers = TRY(decoder.decode<Web::UIEvents::KeyModifier>());
|
||||
auto code_point = TRY(decoder.decode<u32>());
|
||||
auto repeat = TRY(decoder.decode<bool>());
|
||||
auto should_insert_text = TRY(decoder.decode<bool>());
|
||||
|
||||
return Web::KeyEvent { type, key, modifiers, code_point, repeat, nullptr };
|
||||
return Web::KeyEvent { type, key, modifiers, code_point, repeat, should_insert_text, nullptr };
|
||||
}
|
||||
|
||||
template<>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ struct WEB_API KeyEvent {
|
|||
UIEvents::KeyModifier modifiers { UIEvents::KeyModifier::Mod_None };
|
||||
u32 code_point { 0 };
|
||||
bool repeat { false };
|
||||
bool should_insert_text { false };
|
||||
|
||||
OwnPtr<BrowserInputData> browser_data;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -351,9 +351,9 @@ EventResult Page::handle_pinch_event(DevicePixelPoint position, unsigned modifie
|
|||
return top_level_traversable()->event_handler().handle_pinch_event(device_to_css_point(position), modifiers, scale);
|
||||
}
|
||||
|
||||
EventResult Page::handle_keydown(UIEvents::KeyCode key, unsigned modifiers, u32 code_point, bool repeat)
|
||||
EventResult Page::handle_keydown(UIEvents::KeyCode key, unsigned modifiers, u32 code_point, bool repeat, bool should_insert_text)
|
||||
{
|
||||
return focused_navigable().event_handler().handle_keydown(key, modifiers, code_point, repeat);
|
||||
return focused_navigable().event_handler().handle_keydown(key, modifiers, code_point, repeat, should_insert_text);
|
||||
}
|
||||
|
||||
EventResult Page::handle_keyup(UIEvents::KeyCode key, unsigned modifiers, u32 code_point, bool repeat)
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ public:
|
|||
EventResult handle_drag_and_drop_event(DragEvent::Type, DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files);
|
||||
EventResult handle_pinch_event(DevicePixelPoint point, unsigned modifiers, double scale);
|
||||
|
||||
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat, bool should_insert_text);
|
||||
EventResult handle_keyup(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
|
||||
|
||||
void handle_sdl_input_events();
|
||||
|
|
|
|||
|
|
@ -922,6 +922,7 @@ static bool is_shifted_character(u32 code_point)
|
|||
struct KeyEvent {
|
||||
u32 code_point { 0 };
|
||||
UIEvents::KeyModifier modifiers { UIEvents::KeyModifier::Mod_None };
|
||||
bool should_insert_text { false };
|
||||
};
|
||||
static KeyEvent key_code_to_page_event(u32 code_point, UIEvents::KeyModifier modifiers, KeyCodeData const& code)
|
||||
{
|
||||
|
|
@ -962,7 +963,11 @@ static KeyEvent key_code_to_page_event(u32 code_point, UIEvents::KeyModifier mod
|
|||
if (has_flag(modifiers, UIEvents::KeyModifier::Mod_Shift))
|
||||
code_point = code.alternate_key.value_or(code_point);
|
||||
|
||||
return { code_point, modifiers };
|
||||
auto should_insert_text = code_point != 0
|
||||
&& code_point < 0xE000
|
||||
&& !(modifiers & (UIEvents::KeyModifier::Mod_Ctrl | UIEvents::KeyModifier::Mod_Alt | UIEvents::KeyModifier::Mod_Super));
|
||||
|
||||
return { code_point, modifiers, should_insert_text };
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action
|
||||
|
|
@ -1018,7 +1023,7 @@ static ErrorOr<void, WebDriver::Error> dispatch_key_down_action(ActionObject::Ke
|
|||
// keyboard in accordance with the requirements of [UI-EVENTS], and producing the following events, as appropriate,
|
||||
// with the specified properties. This will always produce events including at least a keyDown event.
|
||||
auto event = key_code_to_page_event(raw_key, modifiers, code);
|
||||
browsing_context.page().handle_keydown(code.code, event.modifiers, event.code_point, repeat);
|
||||
browsing_context.page().handle_keydown(code.code, event.modifiers, event.code_point, repeat, event.should_insert_text);
|
||||
|
||||
// 13. Return success with data null.
|
||||
return {};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
keydown key=å keyCodePoints=e5 altKey=true ctrlKey=false
|
||||
beforeinput data=å dataCodePoints=e5 inputType=insertText
|
||||
value after Alt: å
|
||||
keydown key=@ keyCodePoints=40 altKey=true ctrlKey=true
|
||||
beforeinput data=@ dataCodePoints=40 inputType=insertText
|
||||
value after Ctrl+Alt: å@
|
||||
keydown key= keyCodePoints=a0 altKey=true ctrlKey=false
|
||||
beforeinput data= dataCodePoints=a0 inputType=insertText
|
||||
value after Alt+Space: å@
|
||||
value code points after Alt+Space: e5 40 a0
|
||||
keydown key=f keyCodePoints=66 altKey=true ctrlKey=false
|
||||
value after Alt+F shortcut: å@
|
||||
value code points after Alt+F shortcut: e5 40 a0
|
||||
keydown key=x keyCodePoints=78 altKey=false ctrlKey=true
|
||||
value after Ctrl: å@
|
||||
value code points after Ctrl: e5 40 a0
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<input id="input" />
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
test(() => {
|
||||
let input = document.getElementById("input");
|
||||
let codePoints = text => Array.from(text, character => character.codePointAt(0).toString(16)).join(" ");
|
||||
|
||||
input.addEventListener("keydown", e => {
|
||||
println(`keydown key=${e.key} keyCodePoints=${codePoints(e.key)} altKey=${e.altKey} ctrlKey=${e.ctrlKey}`);
|
||||
});
|
||||
|
||||
input.addEventListener("beforeinput", e => {
|
||||
println(`beforeinput data=${e.data} dataCodePoints=${codePoints(e.data)} inputType=${e.inputType}`);
|
||||
});
|
||||
|
||||
internals.sendText(input, "å", internals.MOD_ALT);
|
||||
println(`value after Alt: ${input.value}`);
|
||||
|
||||
internals.sendText(input, "@", internals.MOD_ALT | internals.MOD_CTRL);
|
||||
println(`value after Ctrl+Alt: ${input.value}`);
|
||||
|
||||
internals.sendText(input, "\u00A0", internals.MOD_ALT);
|
||||
println(`value after Alt+Space: ${input.value}`);
|
||||
println(`value code points after Alt+Space: ${codePoints(input.value)}`);
|
||||
|
||||
internals.sendKey(input, "F", internals.MOD_ALT);
|
||||
println(`value after Alt+F shortcut: ${input.value}`);
|
||||
println(`value code points after Alt+F shortcut: ${codePoints(input.value)}`);
|
||||
|
||||
internals.sendText(input, "x", internals.MOD_CTRL);
|
||||
println(`value after Ctrl: ${input.value}`);
|
||||
println(`value code points after Ctrl: ${codePoints(input.value)}`);
|
||||
});
|
||||
</script>
|
||||
|
|
@ -20,7 +20,7 @@ Web::MouseEvent ns_event_to_mouse_event(Web::MouseEvent::Type, NSEvent*, NSView*
|
|||
Web::DragEvent ns_event_to_drag_event(Web::DragEvent::Type, id<NSDraggingInfo>, NSView*);
|
||||
Vector<URL::URL> drag_event_url_list(Web::DragEvent const&);
|
||||
|
||||
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type, NSEvent*);
|
||||
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type, NSEvent*, bool should_insert_text = false);
|
||||
NSEvent* key_event_to_ns_event(Web::KeyEvent const&);
|
||||
|
||||
NSEvent* create_context_menu_mouse_event(NSView*, Gfx::IntPoint);
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ private:
|
|||
CFTypeRef m_event { nullptr };
|
||||
};
|
||||
|
||||
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type type, NSEvent* event)
|
||||
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type type, NSEvent* event, bool should_insert_text)
|
||||
{
|
||||
auto modifiers = ns_modifiers_to_key_modifiers(event.modifierFlags);
|
||||
auto key_code = ns_key_code_to_key_code(event.keyCode, modifiers);
|
||||
|
|
@ -311,7 +311,7 @@ Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type type, NSEvent* event)
|
|||
if (code_point >= 0xE000 && code_point <= 0xF8FF)
|
||||
code_point = 0;
|
||||
|
||||
return { type, key_code, modifiers, code_point, repeat, make<KeyData>(event) };
|
||||
return { type, key_code, modifiers, code_point, repeat, should_insert_text, make<KeyData>(event) };
|
||||
}
|
||||
|
||||
NSEvent* key_event_to_ns_event(Web::KeyEvent const& event)
|
||||
|
|
|
|||
|
|
@ -952,7 +952,7 @@ static Web::DevicePixelPoint node_picker_position_for(Ladybird::WebViewBridge co
|
|||
};
|
||||
}
|
||||
|
||||
- (void)handleCurrentKeyDownEvent
|
||||
- (void)handleCurrentKeyDownEvent:(BOOL)shouldInsertText
|
||||
{
|
||||
if (!self.current_key_down_event)
|
||||
return;
|
||||
|
|
@ -962,7 +962,7 @@ static Web::DevicePixelPoint node_picker_position_for(Ladybird::WebViewBridge co
|
|||
return;
|
||||
}
|
||||
|
||||
auto key_event = Ladybird::ns_event_to_key_event(Web::KeyEvent::Type::KeyDown, self.current_key_down_event);
|
||||
auto key_event = Ladybird::ns_event_to_key_event(Web::KeyEvent::Type::KeyDown, self.current_key_down_event, shouldInsertText);
|
||||
m_web_view_bridge->enqueue_input_event(move(key_event));
|
||||
|
||||
self.current_key_down_event = nil;
|
||||
|
|
@ -1396,12 +1396,12 @@ static Web::DevicePixelPoint node_picker_position_for(Ladybird::WebViewBridge co
|
|||
|
||||
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
|
||||
{
|
||||
[self handleCurrentKeyDownEvent];
|
||||
[self handleCurrentKeyDownEvent:YES];
|
||||
}
|
||||
|
||||
- (void)doCommandBySelector:(SEL)selector
|
||||
{
|
||||
[self handleCurrentKeyDownEvent];
|
||||
[self handleCurrentKeyDownEvent:NO];
|
||||
}
|
||||
|
||||
- (BOOL)hasMarkedText
|
||||
|
|
|
|||
|
|
@ -106,12 +106,19 @@ void WebContentView::enqueue_native_event(Web::MouseEvent::Type type, double x,
|
|||
|
||||
void WebContentView::enqueue_native_event(Web::KeyEvent::Type type, guint keyval, GdkModifierType state)
|
||||
{
|
||||
auto modifiers = gdk_modifier_to_web(state);
|
||||
auto code_point = gdk_keyval_to_unicode(keyval);
|
||||
auto should_insert_text = type == Web::KeyEvent::Type::KeyDown
|
||||
&& code_point != 0
|
||||
&& !(modifiers & (Web::UIEvents::KeyModifier::Mod_Ctrl | Web::UIEvents::KeyModifier::Mod_Alt | Web::UIEvents::KeyModifier::Mod_Super));
|
||||
|
||||
Web::KeyEvent event {
|
||||
.type = type,
|
||||
.key = gdk_keyval_to_web(keyval),
|
||||
.modifiers = gdk_modifier_to_web(state),
|
||||
.code_point = gdk_keyval_to_unicode(keyval),
|
||||
.modifiers = modifiers,
|
||||
.code_point = code_point,
|
||||
.repeat = false,
|
||||
.should_insert_text = should_insert_text,
|
||||
.browser_data = {},
|
||||
};
|
||||
enqueue_input_event(move(event));
|
||||
|
|
|
|||
|
|
@ -1116,19 +1116,20 @@ void WebContentView::enqueue_native_event(Web::KeyEvent::Type type, QKeyEvent co
|
|||
|
||||
auto text = event.text();
|
||||
auto code_point = text.isEmpty() ? 0u : event.text()[0].unicode();
|
||||
auto should_insert_text = type == Web::KeyEvent::Type::KeyDown && !text.isEmpty();
|
||||
|
||||
auto to_web_event = [&]() -> Web::KeyEvent {
|
||||
if (event.key() == Qt::Key_Backtab) {
|
||||
// Qt transforms Shift+Tab into a "Backtab", so we undo that transformation here.
|
||||
return { type, Web::UIEvents::KeyCode::Key_Tab, Web::UIEvents::Mod_Shift, '\t', event.isAutoRepeat(), make<KeyData>(event) };
|
||||
return { type, Web::UIEvents::KeyCode::Key_Tab, Web::UIEvents::Mod_Shift, '\t', event.isAutoRepeat(), false, make<KeyData>(event) };
|
||||
}
|
||||
|
||||
if (event.key() == Qt::Key_Enter || event.key() == Qt::Key_Return) {
|
||||
// This ensures consistent behavior between systems that treat Enter as '\n' and '\r\n'
|
||||
return { type, Web::UIEvents::KeyCode::Key_Return, modifiers, '\n', event.isAutoRepeat(), make<KeyData>(event) };
|
||||
return { type, Web::UIEvents::KeyCode::Key_Return, modifiers, '\n', event.isAutoRepeat(), should_insert_text, make<KeyData>(event) };
|
||||
}
|
||||
|
||||
return { type, keycode, modifiers, code_point, event.isAutoRepeat(), make<KeyData>(event) };
|
||||
return { type, keycode, modifiers, code_point, event.isAutoRepeat(), should_insert_text, make<KeyData>(event) };
|
||||
};
|
||||
|
||||
enqueue_input_event(to_web_event());
|
||||
|
|
|
|||
Loading…
Reference in a new issue