Skip to content

Commit

Permalink
macOS: Improve deadkeys (#20515)
Browse files Browse the repository at this point in the history
Closes #19738

This change refactors how we handle input on macOS to avoid simulating
our own IME. This fixes a number of small edge-cases, and also lets us
remove a bunch of code that had been added to work around bugs in the
previous version.

Release Notes:

- On macOS: Keyboard shortcuts are now handled before activating the IME
system, this enables using vim's default mode on keyboards that use IME
menus (like Japanese).
- On macOS: Improvements to handling of dead-keys. For example when
typing `""` on a Brazillian keyboard, you now get a committed " and a
new marked ", as happens in other apps. Also, you can now type cmd-^ on
an AZERTY keyboard for indent; and ^ on a QWERTZ keyboard now goes to
the beginning of line in vim normal mode, or `d i "` no requires no
space to delete within quotes on Brazilian keyboards (though `d f "
space` is still required as `f` relies on the input handler, not a
binding).
- On macOS: In the terminal pane, holding down a key will now repeat
that key (as happens in iTerm2) instead of opening the character
selector.
  • Loading branch information
ConradIrwin authored Nov 11, 2024
1 parent 38f2a91 commit 2ea4ede
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 213 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/gpui/examples/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ impl Render for InputExample {
.children(self.recent_keystrokes.iter().rev().map(|ks| {
format!(
"{:} {}",
ks,
ks.unparse(),
if let Some(ime_key) = ks.ime_key.as_ref() {
format!("-> {}", ime_key)
} else {
Expand Down
14 changes: 14 additions & 0 deletions crates/gpui/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,11 @@ impl PlatformInputHandler {
.flatten()
}

#[allow(dead_code)]
fn apple_press_and_hold_enabled(&mut self) -> bool {
self.handler.apple_press_and_hold_enabled()
}

pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) {
self.handler.replace_text_in_range(None, input, cx);
}
Expand Down Expand Up @@ -780,6 +785,15 @@ pub trait InputHandler: 'static {
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<Bounds<Pixels>>;

/// Allows a given input context to opt into getting raw key repeats instead of
/// sending these to the platform.
/// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults
/// (which is how iTerm does it) but it doesn't seem to work for me.
#[allow(dead_code)]
fn apple_press_and_hold_enabled(&mut self) -> bool {
true
}
}

/// The variables that can be configured when creating a new window
Expand Down
3 changes: 3 additions & 0 deletions crates/gpui/src/platform/keystroke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ impl Keystroke {
/// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String {
let mut str = String::new();
if self.modifiers.function {
str.push_str("fn-");
}
if self.modifiers.control {
str.push_str("ctrl-");
}
Expand Down
153 changes: 90 additions & 63 deletions crates/gpui/src/platform/mac/events.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
use crate::{
platform::mac::NSStringExt, point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
MouseUpEvent, NavigationDirection, Pixels, PlatformInput, ScrollDelta, ScrollWheelEvent,
TouchPhase,
platform::mac::{
kTISPropertyUnicodeKeyLayoutData, LMGetKbdType, NSStringExt,
TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, UCKeyTranslate,
},
point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
base::{id, YES},
};
use core_graphics::{
event::{CGEvent, CGEventFlags, CGKeyCode},
event_source::{CGEventSource, CGEventSourceStateID},
};
use metal::foreign_types::ForeignType as _;
use objc::{class, msg_send, sel, sel_impl};
use std::{borrow::Cow, mem, ptr, sync::Once};
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
use core_graphics::event::CGKeyCode;
use objc::{msg_send, sel, sel_impl};
use std::{borrow::Cow, ffi::c_void};

const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
Expand All @@ -24,24 +24,6 @@ const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;

fn synthesize_keyboard_event(code: CGKeyCode) -> CGEvent {
static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
static INIT_EVENT_SOURCE: Once = Once::new();

INIT_EVENT_SOURCE.call_once(|| {
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
unsafe {
EVENT_SOURCE = source.as_ptr();
};
mem::forget(source);
});

let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
mem::forget(source);
event
}

pub fn key_to_native(key: &str) -> Cow<str> {
use cocoa::appkit::*;
let code = match key {
Expand Down Expand Up @@ -259,8 +241,9 @@ impl PlatformInput {
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
use cocoa::appkit::*;

let mut chars_ignoring_modifiers = chars_for_modified_key(native_event.keyCode(), false, false);
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
let mut characters = native_event.characters().to_str().to_string();
let mut ime_key = None;
let first_char = characters.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags();

let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
Expand Down Expand Up @@ -321,6 +304,9 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
// * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/)
// * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout)
// * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z)
//
let mut chars_ignoring_modifiers =
chars_for_modified_key(native_event.keyCode(), false, false);
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), false, true);

// Handle Dvorak+QWERTY / Russian / Armeniam
Expand All @@ -341,14 +327,24 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
chars_ignoring_modifiers = chars_with_cmd;
}

if shift && chars_ignoring_modifiers == chars_with_shift.to_ascii_lowercase() {
let mut key = if shift
&& chars_ignoring_modifiers
.chars()
.all(|c| c.is_ascii_lowercase())
{
chars_ignoring_modifiers
} else if shift {
shift = false;
chars_with_shift
} else {
chars_ignoring_modifiers
}
};

if characters.len() > 0 && characters != key {
ime_key = Some(characters.clone());
};

key
}
};

Expand All @@ -361,50 +357,81 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
function,
},
key,
ime_key: None,
ime_key,
}
}

fn always_use_command_layout() -> bool {
// look at the key to the right of "tab" ('a' in QWERTY)
// if it produces a non-ASCII character, but with command held produces ASCII,
// we default to the command layout for our keyboard system.
let event = synthesize_keyboard_event(0);
let without_cmd = unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
};
if without_cmd.is_ascii() {
if chars_for_modified_key(0, false, false).is_ascii() {
return false;
}

event.set_flags(CGEventFlags::CGEventFlagCommand);
let with_cmd = unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
};

with_cmd.is_ascii()
chars_for_modified_key(0, true, false).is_ascii()
}

fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
// an event with the given flags instead lets us access `characters`, which always
// returns a valid string.
let event = synthesize_keyboard_event(code);
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
// shifted >> 8 for UCKeyTranslate
const CMD_MOD: u32 = 1;
const SHIFT_MOD: u32 = 2;
const CG_SPACE_KEY: u16 = 49;
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
#[allow(non_upper_case_globals)]
const kUCKeyActionDown: u16 = 0;
#[allow(non_upper_case_globals)]
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;

let mut flags = CGEventFlags::empty();
if cmd {
flags |= CGEventFlags::CGEventFlagCommand;
let keyboard_type = unsafe { LMGetKbdType() as u32 };
const BUFFER_SIZE: usize = 4;
let mut dead_key_state = 0;
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut buffer_size: usize = 0;

let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
if keyboard.is_null() {
return "".to_string();
}
if shift {
flags |= CGEventFlags::CGEventFlagShift;
let layout_data = unsafe {
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
as CFDataRef
};
if layout_data.is_null() {
unsafe {
let _: () = msg_send![keyboard, release];
}
return "".to_string();
}
event.set_flags(flags);
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
let modifiers = if cmd { CMD_MOD } else { 0 } | if shift { SHIFT_MOD } else { 0 };

unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
UCKeyTranslate(
keyboard_layout as *const c_void,
code,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
if dead_key_state != 0 {
UCKeyTranslate(
keyboard_layout as *const c_void,
CG_SPACE_KEY,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
}
let _: () = msg_send![keyboard, release];
}
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
}
20 changes: 17 additions & 3 deletions crates/gpui/src/platform/mac/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1448,13 +1448,27 @@ unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {

#[link(name = "Carbon", kind = "framework")]
extern "C" {
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
fn TISGetInputSourceProperty(
pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
pub(super) fn TISGetInputSourceProperty(
inputSource: *mut Object,
propertyKey: *const c_void,
) -> *mut Object;

pub static kTISPropertyInputSourceID: CFStringRef;
pub(super) fn UCKeyTranslate(
keyLayoutPtr: *const ::std::os::raw::c_void,
virtualKeyCode: u16,
keyAction: u16,
modifierKeyState: u32,
keyboardType: u32,
keyTranslateOptions: u32,
deadKeyState: *mut u32,
maxStringLength: usize,
actualStringLength: *mut usize,
unicodeString: *mut u16,
) -> u32;
pub(super) fn LMGetKbdType() -> u16;
pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
pub(super) static kTISPropertyInputSourceID: CFStringRef;
}

mod security {
Expand Down
Loading

0 comments on commit 2ea4ede

Please sign in to comment.