Skip to content

Commit

Permalink
Deadkeys 2 (#20612)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ConradIrwin authored Nov 13, 2024
1 parent ad31aac commit 96deabf
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 220 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.

4 changes: 2 additions & 2 deletions crates/gpui/examples/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,9 +580,9 @@ 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)
format!("-> {:?}", ime_key)
} else {
"".to_owned()
}
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 @@ -688,6 +688,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 @@ -785,6 +790,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
4 changes: 4 additions & 0 deletions crates/gpui/src/platform/keystroke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ impl Keystroke {
{
let ime_modifiers = Modifiers {
control: self.modifiers.control,
platform: self.modifiers.platform,
..Default::default()
};

Expand Down Expand Up @@ -124,6 +125,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
182 changes: 113 additions & 69 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,12 @@ 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
.charactersIgnoringModifiers()
.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 @@ -313,20 +299,25 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
_ => {
// Cases to test when modifying this:
//
// qwerty key | none | cmd | cmd-shift
// qwerty key | none | cmd | cmd-shift
// * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout)
// * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd)
// * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S)
// * Czech 7 | ý | cmd-ý | cmd-7 (layout has shifted numbers)
// * 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_with_shift = chars_for_modified_key(native_event.keyCode(), false, true);
//
let mut chars_ignoring_modifiers =
chars_for_modified_key(native_event.keyCode(), NO_MOD);
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), SHIFT_MOD);
let always_use_cmd_layout = always_use_command_layout();

// Handle Dvorak+QWERTY / Russian / Armeniam
if command || always_use_command_layout() {
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
let chars_with_both = chars_for_modified_key(native_event.keyCode(), true, true);
if command || always_use_cmd_layout {
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
let chars_with_both =
chars_for_modified_key(native_event.keyCode(), CMD_MOD | SHIFT_MOD);

// We don't do this in the case that the shifted command key generates
// the same character as the unshifted command key (Norwegian, e.g.)
Expand All @@ -341,14 +332,34 @@ 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 always_use_cmd_layout || alt {
let mut mods = NO_MOD;
if shift {
mods |= SHIFT_MOD;
}
if alt {
mods |= OPTION_MOD;
}
let alt_key = chars_for_modified_key(native_event.keyCode(), mods);
if alt_key != key {
ime_key = Some(alt_key);
}
};

key
}
};

Expand All @@ -361,50 +372,83 @@ 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, NO_MOD).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, CMD_MOD).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);
const NO_MOD: u32 = 0;
const CMD_MOD: u32 = 1;
const SHIFT_MOD: u32 = 2;
const OPTION_MOD: u32 = 8;

fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
// 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 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 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 mut flags = CGEventFlags::empty();
if cmd {
flags |= CGEventFlags::CGEventFlagCommand;
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) };

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 96deabf

Please sign in to comment.