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
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5661,7 +5661,7 @@ dependencies = [
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.4.10",
|
"socket2 0.5.7",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
@ -580,9 +580,9 @@ impl Render for InputExample {
|
||||||
.children(self.recent_keystrokes.iter().rev().map(|ks| {
|
.children(self.recent_keystrokes.iter().rev().map(|ks| {
|
||||||
format!(
|
format!(
|
||||||
"{:} {}",
|
"{:} {}",
|
||||||
ks,
|
ks.unparse(),
|
||||||
if let Some(ime_key) = ks.ime_key.as_ref() {
|
if let Some(ime_key) = ks.ime_key.as_ref() {
|
||||||
format!("-> {}", ime_key)
|
format!("-> {:?}", ime_key)
|
||||||
} else {
|
} else {
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
}
|
}
|
||||||
|
|
|
@ -688,6 +688,11 @@ impl PlatformInputHandler {
|
||||||
.flatten()
|
.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) {
|
pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) {
|
||||||
self.handler.replace_text_in_range(None, input, cx);
|
self.handler.replace_text_in_range(None, input, cx);
|
||||||
}
|
}
|
||||||
|
@ -785,6 +790,15 @@ pub trait InputHandler: 'static {
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Option<Bounds<Pixels>>;
|
) -> 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
|
/// The variables that can be configured when creating a new window
|
||||||
|
|
|
@ -34,6 +34,7 @@ impl Keystroke {
|
||||||
{
|
{
|
||||||
let ime_modifiers = Modifiers {
|
let ime_modifiers = Modifiers {
|
||||||
control: self.modifiers.control,
|
control: self.modifiers.control,
|
||||||
|
platform: self.modifiers.platform,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,6 +125,9 @@ impl Keystroke {
|
||||||
/// Produces a representation of this key that Parse can understand.
|
/// Produces a representation of this key that Parse can understand.
|
||||||
pub fn unparse(&self) -> String {
|
pub fn unparse(&self) -> String {
|
||||||
let mut str = String::new();
|
let mut str = String::new();
|
||||||
|
if self.modifiers.function {
|
||||||
|
str.push_str("fn-");
|
||||||
|
}
|
||||||
if self.modifiers.control {
|
if self.modifiers.control {
|
||||||
str.push_str("ctrl-");
|
str.push_str("ctrl-");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
platform::mac::NSStringExt, point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
|
platform::mac::{
|
||||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
|
kTISPropertyUnicodeKeyLayoutData, LMGetKbdType, NSStringExt,
|
||||||
MouseUpEvent, NavigationDirection, Pixels, PlatformInput, ScrollDelta, ScrollWheelEvent,
|
TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, UCKeyTranslate,
|
||||||
TouchPhase,
|
},
|
||||||
|
point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||||
|
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
|
||||||
|
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||||
};
|
};
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||||
base::{id, YES},
|
base::{id, YES},
|
||||||
};
|
};
|
||||||
use core_graphics::{
|
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
|
||||||
event::{CGEvent, CGEventFlags, CGKeyCode},
|
use core_graphics::event::CGKeyCode;
|
||||||
event_source::{CGEventSource, CGEventSourceStateID},
|
use objc::{msg_send, sel, sel_impl};
|
||||||
};
|
use std::{borrow::Cow, ffi::c_void};
|
||||||
use metal::foreign_types::ForeignType as _;
|
|
||||||
use objc::{class, msg_send, sel, sel_impl};
|
|
||||||
use std::{borrow::Cow, mem, ptr, sync::Once};
|
|
||||||
|
|
||||||
const BACKSPACE_KEY: u16 = 0x7f;
|
const BACKSPACE_KEY: u16 = 0x7f;
|
||||||
const SPACE_KEY: u16 = b' ' as u16;
|
const SPACE_KEY: u16 = b' ' as u16;
|
||||||
|
@ -24,24 +24,6 @@ const ESCAPE_KEY: u16 = 0x1b;
|
||||||
const TAB_KEY: u16 = 0x09;
|
const TAB_KEY: u16 = 0x09;
|
||||||
const SHIFT_TAB_KEY: u16 = 0x19;
|
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> {
|
pub fn key_to_native(key: &str) -> Cow<str> {
|
||||||
use cocoa::appkit::*;
|
use cocoa::appkit::*;
|
||||||
let code = match key {
|
let code = match key {
|
||||||
|
@ -259,8 +241,12 @@ impl PlatformInput {
|
||||||
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||||
use cocoa::appkit::*;
|
use cocoa::appkit::*;
|
||||||
|
|
||||||
let mut chars_ignoring_modifiers = chars_for_modified_key(native_event.keyCode(), false, false);
|
let mut characters = native_event
|
||||||
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
|
.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 modifiers = native_event.modifierFlags();
|
||||||
|
|
||||||
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
|
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
|
||||||
|
@ -313,7 +299,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||||
_ => {
|
_ => {
|
||||||
// Cases to test when modifying this:
|
// 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)
|
// * 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)
|
// * 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)
|
// * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S)
|
||||||
|
@ -321,12 +307,17 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||||
// * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/)
|
// * 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)
|
// * 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)
|
// * 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
|
// Handle Dvorak+QWERTY / Russian / Armeniam
|
||||||
if command || always_use_command_layout() {
|
if command || always_use_cmd_layout {
|
||||||
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
|
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
|
||||||
let chars_with_both = chars_for_modified_key(native_event.keyCode(), true, true);
|
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
|
// 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.)
|
// the same character as the unshifted command key (Norwegian, e.g.)
|
||||||
|
@ -341,14 +332,34 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||||
chars_ignoring_modifiers = chars_with_cmd;
|
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
|
chars_ignoring_modifiers
|
||||||
} else if shift {
|
} else if shift {
|
||||||
shift = false;
|
shift = false;
|
||||||
chars_with_shift
|
chars_with_shift
|
||||||
} else {
|
} else {
|
||||||
chars_ignoring_modifiers
|
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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -361,50 +372,83 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||||
function,
|
function,
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
ime_key: None,
|
ime_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn always_use_command_layout() -> bool {
|
fn always_use_command_layout() -> bool {
|
||||||
// look at the key to the right of "tab" ('a' in QWERTY)
|
if chars_for_modified_key(0, NO_MOD).is_ascii() {
|
||||||
// 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() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.set_flags(CGEventFlags::CGEventFlagCommand);
|
chars_for_modified_key(0, CMD_MOD).is_ascii()
|
||||||
let with_cmd = unsafe {
|
|
||||||
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
|
|
||||||
event.characters().to_str().to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
with_cmd.is_ascii()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
|
const NO_MOD: u32 = 0;
|
||||||
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
|
const CMD_MOD: u32 = 1;
|
||||||
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
|
const SHIFT_MOD: u32 = 2;
|
||||||
// an event with the given flags instead lets us access `characters`, which always
|
const OPTION_MOD: u32 = 8;
|
||||||
// returns a valid string.
|
|
||||||
let event = synthesize_keyboard_event(code);
|
|
||||||
|
|
||||||
let mut flags = CGEventFlags::empty();
|
fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
|
||||||
if cmd {
|
// 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
|
||||||
flags |= CGEventFlags::CGEventFlagCommand;
|
// 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 keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
|
||||||
|
if keyboard.is_null() {
|
||||||
|
return "".to_string();
|
||||||
}
|
}
|
||||||
if shift {
|
let layout_data = unsafe {
|
||||||
flags |= CGEventFlags::CGEventFlagShift;
|
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 {
|
unsafe {
|
||||||
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
|
UCKeyTranslate(
|
||||||
event.characters().to_str().to_string()
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1448,13 +1448,27 @@ unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
|
||||||
|
|
||||||
#[link(name = "Carbon", kind = "framework")]
|
#[link(name = "Carbon", kind = "framework")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
|
pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
|
||||||
fn TISGetInputSourceProperty(
|
pub(super) fn TISGetInputSourceProperty(
|
||||||
inputSource: *mut Object,
|
inputSource: *mut Object,
|
||||||
propertyKey: *const c_void,
|
propertyKey: *const c_void,
|
||||||
) -> *mut Object;
|
) -> *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 {
|
mod security {
|
||||||
|
|
|
@ -38,7 +38,6 @@ use std::{
|
||||||
cell::Cell,
|
cell::Cell,
|
||||||
ffi::{c_void, CStr},
|
ffi::{c_void, CStr},
|
||||||
mem,
|
mem,
|
||||||
ops::Range,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
ptr::{self, NonNull},
|
ptr::{self, NonNull},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
@ -310,14 +309,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
||||||
decl.register()
|
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 {
|
struct MacWindowState {
|
||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
executor: ForegroundExecutor,
|
executor: ForegroundExecutor,
|
||||||
|
@ -338,14 +329,11 @@ struct MacWindowState {
|
||||||
synthetic_drag_counter: usize,
|
synthetic_drag_counter: usize,
|
||||||
traffic_light_position: Option<Point<Pixels>>,
|
traffic_light_position: Option<Point<Pixels>>,
|
||||||
previous_modifiers_changed_event: Option<PlatformInput>,
|
previous_modifiers_changed_event: Option<PlatformInput>,
|
||||||
// State tracking what the IME did after the last request
|
keystroke_for_do_command: Option<Keystroke>,
|
||||||
last_ime_inputs: Option<SmallVec<[(String, Option<Range<usize>>); 1]>>,
|
|
||||||
previous_keydown_inserted_text: Option<String>,
|
|
||||||
external_files_dragged: bool,
|
external_files_dragged: bool,
|
||||||
// Whether the next left-mouse click is also the focusing click.
|
// Whether the next left-mouse click is also the focusing click.
|
||||||
first_mouse: bool,
|
first_mouse: bool,
|
||||||
fullscreen_restore_bounds: Bounds<Pixels>,
|
fullscreen_restore_bounds: Bounds<Pixels>,
|
||||||
ime_composing: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MacWindowState {
|
impl MacWindowState {
|
||||||
|
@ -619,12 +607,10 @@ impl MacWindow {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|titlebar| titlebar.traffic_light_position),
|
.and_then(|titlebar| titlebar.traffic_light_position),
|
||||||
previous_modifiers_changed_event: None,
|
previous_modifiers_changed_event: None,
|
||||||
last_ime_inputs: None,
|
keystroke_for_do_command: None,
|
||||||
previous_keydown_inserted_text: None,
|
|
||||||
external_files_dragged: false,
|
external_files_dragged: false,
|
||||||
first_mouse: false,
|
first_mouse: false,
|
||||||
fullscreen_restore_bounds: Bounds::default(),
|
fullscreen_restore_bounds: Bounds::default(),
|
||||||
ime_composing: false,
|
|
||||||
})));
|
})));
|
||||||
|
|
||||||
(*native_window).set_ivar(
|
(*native_window).set_ivar(
|
||||||
|
@ -1226,9 +1212,9 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
|
||||||
// Brazilian layout:
|
// Brazilian layout:
|
||||||
// - `" space` should create an unmarked quote
|
// - `" space` should create an unmarked quote
|
||||||
// - `" backspace` should delete the marked 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
|
// - `" 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
|
// - `" 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
|
// - `cmd-ctrl-space` and clicking on an emoji should type it
|
||||||
// Czech (QWERTY) layout:
|
// Czech (QWERTY) layout:
|
||||||
// - in vim mode `option-4` should go to end of line (same as $)
|
// - 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 window_height = lock.content_size().height;
|
||||||
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
|
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
|
||||||
|
|
||||||
if let Some(PlatformInput::KeyDown(mut event)) = event {
|
let Some(PlatformInput::KeyDown(mut event)) = event else {
|
||||||
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
|
return NO;
|
||||||
// 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
|
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
|
||||||
// the "key down" event if we've already just processed its "key equivalent" version.
|
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
|
||||||
if key_equivalent {
|
// makes no distinction between these two types of events, so we need to ignore
|
||||||
lock.last_key_equivalent = Some(event.clone());
|
// the "key down" event if we've already just processed its "key equivalent" version.
|
||||||
} else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
|
if key_equivalent {
|
||||||
return NO;
|
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 mut callback = window_state.as_ref().lock().event_callback.take();
|
||||||
let fn_modifier = keydown.modifiers.function;
|
let handled: BOOL = if let Some(callback) = callback.as_mut() {
|
||||||
lock.last_ime_inputs = Some(Default::default());
|
!callback(PlatformInput::KeyDown(event)).propagate as BOOL
|
||||||
drop(lock);
|
} 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
|
let mut callback = window_state.as_ref().lock().event_callback.take();
|
||||||
// being pressed.
|
let handled = if let Some(callback) = callback.as_mut() {
|
||||||
// this will call back into `insert_text`, etc.
|
!callback(PlatformInput::KeyDown(event.clone())).propagate as BOOL
|
||||||
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
|
|
||||||
} else {
|
} else {
|
||||||
NO
|
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 text = text.to_str();
|
||||||
let replacement_range = replacement_range.to_range();
|
let replacement_range = replacement_range.to_range();
|
||||||
send_to_input_handler(
|
with_input_handler(this, |input_handler| {
|
||||||
this,
|
input_handler.replace_text_in_range(replacement_range, &text)
|
||||||
ImeInput::InsertText(text.to_string(), replacement_range),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1766,15 +1736,13 @@ extern "C" fn set_marked_text(
|
||||||
let selected_range = selected_range.to_range();
|
let selected_range = selected_range.to_range();
|
||||||
let replacement_range = replacement_range.to_range();
|
let replacement_range = replacement_range.to_range();
|
||||||
let text = text.to_str();
|
let text = text.to_str();
|
||||||
|
with_input_handler(this, |input_handler| {
|
||||||
send_to_input_handler(
|
input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range)
|
||||||
this,
|
});
|
||||||
ImeInput::SetMarkedText(text.to_string(), replacement_range, selected_range),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
extern "C" fn unmark_text(this: &Object, _: Sel) {
|
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(
|
extern "C" fn attributed_substring_for_proposed_range(
|
||||||
|
@ -1800,7 +1768,24 @@ extern "C" fn attributed_substring_for_proposed_range(
|
||||||
.unwrap_or(nil)
|
.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) {
|
extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
|
||||||
unsafe {
|
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 {
|
unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
|
||||||
let device_description = NSScreen::deviceDescription(screen);
|
let device_description = NSScreen::deviceDescription(screen);
|
||||||
let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
|
let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
|
||||||
|
|
|
@ -1044,6 +1044,10 @@ impl InputHandler for TerminalInputHandler {
|
||||||
) -> Option<Bounds<Pixels>> {
|
) -> Option<Bounds<Pixels>> {
|
||||||
self.cursor_bounds
|
self.cursor_bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apple_press_and_hold_enabled(&mut self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_blank(cell: &IndexedCell) -> bool {
|
pub fn is_blank(cell: &IndexedCell) -> bool {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue