Deadkeys 2 (#20612)
Re-land of #20515 with less brokenness In particular it turns out that for control, the .characters() method returns the control code. This mostly didn't make a difference, except when the control code matched tab/enter/escape (for ctrl-y,ctrl-[/ctrl-c) as we interpreted the key incorrectly. Secondly, we were setting IME key too aggressively. This led to (in vim mode) cmd-shift-{ being interpreted as [, so vim would wait for a second [ before letting you change tab. Release Notes: - N/A
This commit is contained in:
parent
ad31aacb7a
commit
96deabfb78
8 changed files with 252 additions and 224 deletions
|
@ -38,7 +38,6 @@ use std::{
|
|||
cell::Cell,
|
||||
ffi::{c_void, CStr},
|
||||
mem,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
ptr::{self, NonNull},
|
||||
rc::Rc,
|
||||
|
@ -310,14 +309,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
|||
decl.register()
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Clone, Debug)]
|
||||
enum ImeInput {
|
||||
InsertText(String, Option<Range<usize>>),
|
||||
SetMarkedText(String, Option<Range<usize>>, Option<Range<usize>>),
|
||||
UnmarkText,
|
||||
}
|
||||
|
||||
struct MacWindowState {
|
||||
handle: AnyWindowHandle,
|
||||
executor: ForegroundExecutor,
|
||||
|
@ -338,14 +329,11 @@ struct MacWindowState {
|
|||
synthetic_drag_counter: usize,
|
||||
traffic_light_position: Option<Point<Pixels>>,
|
||||
previous_modifiers_changed_event: Option<PlatformInput>,
|
||||
// State tracking what the IME did after the last request
|
||||
last_ime_inputs: Option<SmallVec<[(String, Option<Range<usize>>); 1]>>,
|
||||
previous_keydown_inserted_text: Option<String>,
|
||||
keystroke_for_do_command: Option<Keystroke>,
|
||||
external_files_dragged: bool,
|
||||
// Whether the next left-mouse click is also the focusing click.
|
||||
first_mouse: bool,
|
||||
fullscreen_restore_bounds: Bounds<Pixels>,
|
||||
ime_composing: bool,
|
||||
}
|
||||
|
||||
impl MacWindowState {
|
||||
|
@ -619,12 +607,10 @@ impl MacWindow {
|
|||
.as_ref()
|
||||
.and_then(|titlebar| titlebar.traffic_light_position),
|
||||
previous_modifiers_changed_event: None,
|
||||
last_ime_inputs: None,
|
||||
previous_keydown_inserted_text: None,
|
||||
keystroke_for_do_command: None,
|
||||
external_files_dragged: false,
|
||||
first_mouse: false,
|
||||
fullscreen_restore_bounds: Bounds::default(),
|
||||
ime_composing: false,
|
||||
})));
|
||||
|
||||
(*native_window).set_ivar(
|
||||
|
@ -1226,9 +1212,9 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
|
|||
// Brazilian layout:
|
||||
// - `" space` should create an unmarked quote
|
||||
// - `" backspace` should delete the marked quote
|
||||
// - `" "`should create an unmarked quote and a second marked quote
|
||||
// - `" up` should insert a quote, unmark it, and move up one line
|
||||
// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
|
||||
// - NOTE: The current implementation does not move the selection to the end of the file
|
||||
// - `cmd-ctrl-space` and clicking on an emoji should type it
|
||||
// Czech (QWERTY) layout:
|
||||
// - in vim mode `option-4` should go to end of line (same as $)
|
||||
|
@ -1241,95 +1227,80 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
|||
let window_height = lock.content_size().height;
|
||||
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
|
||||
|
||||
if let Some(PlatformInput::KeyDown(mut event)) = event {
|
||||
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
|
||||
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
|
||||
// makes no distinction between these two types of events, so we need to ignore
|
||||
// the "key down" event if we've already just processed its "key equivalent" version.
|
||||
if key_equivalent {
|
||||
lock.last_key_equivalent = Some(event.clone());
|
||||
} else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
|
||||
return NO;
|
||||
let Some(PlatformInput::KeyDown(mut event)) = event else {
|
||||
return NO;
|
||||
};
|
||||
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
|
||||
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
|
||||
// makes no distinction between these two types of events, so we need to ignore
|
||||
// the "key down" event if we've already just processed its "key equivalent" version.
|
||||
if key_equivalent {
|
||||
lock.last_key_equivalent = Some(event.clone());
|
||||
} else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
drop(lock);
|
||||
|
||||
let is_composing = with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
// If we're composing, send the key to the input handler first;
|
||||
// otherwise we only send to the input handler if we don't have a matching binding.
|
||||
// The input handler may call `do_command_by_selector` if it doesn't know how to handle
|
||||
// a key. If it does so, it will return YES so we won't send the key twice.
|
||||
if is_composing || event.keystroke.key.is_empty() {
|
||||
window_state.as_ref().lock().keystroke_for_do_command = Some(event.keystroke.clone());
|
||||
let handled: BOOL = unsafe {
|
||||
let input_context: id = msg_send![this, inputContext];
|
||||
msg_send![input_context, handleEvent: native_event]
|
||||
};
|
||||
window_state.as_ref().lock().keystroke_for_do_command.take();
|
||||
if handled == YES {
|
||||
return YES;
|
||||
}
|
||||
|
||||
let keydown = event.keystroke.clone();
|
||||
let fn_modifier = keydown.modifiers.function;
|
||||
lock.last_ime_inputs = Some(Default::default());
|
||||
drop(lock);
|
||||
let mut callback = window_state.as_ref().lock().event_callback.take();
|
||||
let handled: BOOL = if let Some(callback) = callback.as_mut() {
|
||||
!callback(PlatformInput::KeyDown(event)).propagate as BOOL
|
||||
} else {
|
||||
NO
|
||||
};
|
||||
window_state.as_ref().lock().event_callback = callback;
|
||||
return handled as BOOL;
|
||||
}
|
||||
|
||||
// Send the event to the input context for IME handling, unless the `fn` modifier is
|
||||
// being pressed.
|
||||
// this will call back into `insert_text`, etc.
|
||||
if !fn_modifier {
|
||||
unsafe {
|
||||
let input_context: id = msg_send![this, inputContext];
|
||||
let _: BOOL = msg_send![input_context, handleEvent: native_event];
|
||||
}
|
||||
}
|
||||
|
||||
let mut handled = false;
|
||||
let mut lock = window_state.lock();
|
||||
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
|
||||
let mut last_inserts = lock.last_ime_inputs.take().unwrap();
|
||||
let ime_composing = std::mem::take(&mut lock.ime_composing);
|
||||
|
||||
let mut callback = lock.event_callback.take();
|
||||
drop(lock);
|
||||
|
||||
let last_insert = last_inserts.pop();
|
||||
// on a brazilian keyboard typing `"` and then hitting `up` will cause two IME
|
||||
// events, one to unmark the quote, and one to send the up arrow.
|
||||
for (text, range) in last_inserts {
|
||||
send_to_input_handler(this, ImeInput::InsertText(text, range));
|
||||
}
|
||||
|
||||
let is_composing =
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.is_some()
|
||||
|| ime_composing;
|
||||
|
||||
if let Some((text, range)) = last_insert {
|
||||
if !is_composing {
|
||||
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
|
||||
if let Some(callback) = callback.as_mut() {
|
||||
event.keystroke.ime_key = Some(text.clone());
|
||||
handled = !callback(PlatformInput::KeyDown(event)).propagate;
|
||||
}
|
||||
}
|
||||
|
||||
if !handled {
|
||||
handled = true;
|
||||
send_to_input_handler(this, ImeInput::InsertText(text, range));
|
||||
}
|
||||
} else if !is_composing {
|
||||
let is_held = event.is_held;
|
||||
|
||||
if let Some(callback) = callback.as_mut() {
|
||||
handled = !callback(PlatformInput::KeyDown(event)).propagate;
|
||||
}
|
||||
|
||||
if !handled && is_held {
|
||||
if let Some(text) = previous_keydown_inserted_text {
|
||||
// macOS IME is a bit funky, and even when you've told it there's nothing to
|
||||
// enter it will still swallow certain keys (e.g. 'f', 'j') and not others
|
||||
// (e.g. 'n'). This is a problem for certain kinds of views, like the terminal.
|
||||
with_input_handler(this, |input_handler| {
|
||||
if input_handler.selected_text_range(false).is_none() {
|
||||
handled = true;
|
||||
input_handler.replace_text_in_range(None, &text)
|
||||
}
|
||||
});
|
||||
window_state.lock().previous_keydown_inserted_text = Some(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window_state.lock().event_callback = callback;
|
||||
|
||||
handled as BOOL
|
||||
let mut callback = window_state.as_ref().lock().event_callback.take();
|
||||
let handled = if let Some(callback) = callback.as_mut() {
|
||||
!callback(PlatformInput::KeyDown(event.clone())).propagate as BOOL
|
||||
} else {
|
||||
NO
|
||||
};
|
||||
window_state.as_ref().lock().event_callback = callback;
|
||||
if handled == YES {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if event.is_held {
|
||||
let handled = with_input_handler(&this, |input_handler| {
|
||||
if !input_handler.apple_press_and_hold_enabled() {
|
||||
input_handler.replace_text_in_range(
|
||||
None,
|
||||
&event.keystroke.ime_key.unwrap_or(event.keystroke.key),
|
||||
);
|
||||
return YES;
|
||||
}
|
||||
NO
|
||||
});
|
||||
if handled == Some(YES) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let input_context: id = msg_send![this, inputContext];
|
||||
msg_send![input_context, handleEvent: native_event]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1741,10 +1712,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
|
|||
|
||||
let text = text.to_str();
|
||||
let replacement_range = replacement_range.to_range();
|
||||
send_to_input_handler(
|
||||
this,
|
||||
ImeInput::InsertText(text.to_string(), replacement_range),
|
||||
);
|
||||
with_input_handler(this, |input_handler| {
|
||||
input_handler.replace_text_in_range(replacement_range, &text)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1766,15 +1736,13 @@ extern "C" fn set_marked_text(
|
|||
let selected_range = selected_range.to_range();
|
||||
let replacement_range = replacement_range.to_range();
|
||||
let text = text.to_str();
|
||||
|
||||
send_to_input_handler(
|
||||
this,
|
||||
ImeInput::SetMarkedText(text.to_string(), replacement_range, selected_range),
|
||||
);
|
||||
with_input_handler(this, |input_handler| {
|
||||
input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range)
|
||||
});
|
||||
}
|
||||
}
|
||||
extern "C" fn unmark_text(this: &Object, _: Sel) {
|
||||
send_to_input_handler(this, ImeInput::UnmarkText);
|
||||
with_input_handler(this, |input_handler| input_handler.unmark_text());
|
||||
}
|
||||
|
||||
extern "C" fn attributed_substring_for_proposed_range(
|
||||
|
@ -1800,7 +1768,24 @@ extern "C" fn attributed_substring_for_proposed_range(
|
|||
.unwrap_or(nil)
|
||||
}
|
||||
|
||||
extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) {}
|
||||
// We ignore which selector it asks us to do because the user may have
|
||||
// bound the shortcut to something else.
|
||||
extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
|
||||
let state = unsafe { get_window_state(this) };
|
||||
let mut lock = state.as_ref().lock();
|
||||
let keystroke = lock.keystroke_for_do_command.take();
|
||||
let mut event_callback = lock.event_callback.take();
|
||||
drop(lock);
|
||||
|
||||
if let Some((keystroke, mut callback)) = keystroke.zip(event_callback.as_mut()) {
|
||||
(callback)(PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
}));
|
||||
}
|
||||
|
||||
state.as_ref().lock().event_callback = event_callback;
|
||||
}
|
||||
|
||||
extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
|
@ -1950,43 +1935,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn send_to_input_handler(window: &Object, ime: ImeInput) {
|
||||
unsafe {
|
||||
let window_state = get_window_state(window);
|
||||
let mut lock = window_state.lock();
|
||||
|
||||
if let Some(mut input_handler) = lock.input_handler.take() {
|
||||
match ime {
|
||||
ImeInput::InsertText(text, range) => {
|
||||
if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
|
||||
ime_input.push((text, range));
|
||||
lock.input_handler = Some(input_handler);
|
||||
return;
|
||||
}
|
||||
drop(lock);
|
||||
input_handler.replace_text_in_range(range, &text)
|
||||
}
|
||||
ImeInput::SetMarkedText(text, range, marked_range) => {
|
||||
lock.ime_composing = true;
|
||||
drop(lock);
|
||||
input_handler.replace_and_mark_text_in_range(range, &text, marked_range)
|
||||
}
|
||||
ImeInput::UnmarkText => {
|
||||
drop(lock);
|
||||
input_handler.unmark_text()
|
||||
}
|
||||
}
|
||||
window_state.lock().input_handler = Some(input_handler);
|
||||
} else {
|
||||
if let ImeInput::InsertText(text, range) = ime {
|
||||
if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
|
||||
ime_input.push((text, range));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
|
||||
let device_description = NSScreen::deviceDescription(screen);
|
||||
let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue