macOS: Add key equivalents for non-Latin layouts (#20401)

Closes  #16343
Closes #10972

Release Notes:

- (breaking change) On macOS when using a keyboard that supports an
extended Latin character set (e.g. French, German, ...) keyboard
shortcuts are automatically updated so that they can be typed without
`option`. This fixes several long-standing problems where some keyboards
could not type some shortcuts.
- This mapping works the same way as
[macOS](https://developer.apple.com/documentation/swiftui/view/keyboardshortcut(_:modifiers:localization:)).
For example on a German keyboard shortcuts like `cmd->` become `cmd-:`,
`cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`. This mapping happens at
the time keyboard layout files are read so the keybindings are visible
in the command palette. To opt out of this behavior for your custom
keyboard shortcuts, set `"use_layout_keys": true` in your binding
section. For the mappings used for each layout [see
here](a890df1863/crates/settings/src/key_equivalents.rs (L7)).

---------

Co-authored-by: Will <will@zed.dev>
This commit is contained in:
Conrad Irwin 2024-11-08 13:05:10 -07:00 committed by GitHub
parent 07821083df
commit ff4f67993b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 435 additions and 10 deletions

View file

@ -136,6 +136,11 @@ unsafe fn build_classes() {
open_urls as extern "C" fn(&mut Object, Sel, id, id),
);
decl.add_method(
sel!(onKeyboardLayoutChange:),
on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id),
);
decl.register()
}
}
@ -152,6 +157,7 @@ pub(crate) struct MacPlatformState {
text_hash_pasteboard_type: id,
metadata_pasteboard_type: id,
reopen: Option<Box<dyn FnMut()>>,
on_keyboard_layout_change: Option<Box<dyn FnMut()>>,
quit: Option<Box<dyn FnMut()>>,
menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
@ -196,6 +202,7 @@ impl MacPlatform {
open_urls: None,
finish_launching: None,
dock_menu: None,
on_keyboard_layout_change: None,
}))
}
@ -785,6 +792,10 @@ impl Platform for MacPlatform {
self.0.lock().reopen = Some(callback);
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.0.lock().on_keyboard_layout_change = Some(callback);
}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.0.lock().menu_command = Some(callback);
}
@ -797,6 +808,22 @@ impl Platform for MacPlatform {
self.0.lock().validate_menu_command = Some(callback);
}
fn keyboard_layout(&self) -> String {
unsafe {
let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource();
let input_source_id: *mut Object = TISGetInputSourceProperty(
current_keyboard,
kTISPropertyInputSourceID as *const c_void,
);
let input_source_id: *const std::os::raw::c_char =
msg_send![input_source_id, UTF8String];
let input_source_id = CStr::from_ptr(input_source_id).to_str().unwrap();
input_source_id.to_string()
}
}
fn app_path(&self) -> Result<PathBuf> {
unsafe {
let bundle: id = NSBundle::mainBundle();
@ -1259,6 +1286,16 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
let notification_center: *mut Object =
msg_send![class!(NSNotificationCenter), defaultCenter];
let name = ns_string("NSTextInputContextKeyboardSelectionDidChangeNotification");
let _: () = msg_send![notification_center, addObserver: this as id
selector: sel!(onKeyboardLayoutChange:)
name: name
object: nil
];
let platform = get_mac_platform(this);
let callback = platform.0.lock().finish_launching.take();
if let Some(callback) = callback {
@ -1289,6 +1326,20 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
}
}
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
let platform = unsafe { get_mac_platform(this) };
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
drop(lock);
callback();
platform
.0
.lock()
.on_keyboard_layout_change
.get_or_insert(callback);
}
}
extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
let urls = unsafe {
(0..urls.count())
@ -1395,6 +1446,17 @@ unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
}
}
#[link(name = "Carbon", kind = "framework")]
extern "C" {
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
fn TISGetInputSourceProperty(
inputSource: *mut Object,
propertyKey: *const c_void,
) -> *mut Object;
pub static kTISPropertyInputSourceID: CFStringRef;
}
mod security {
#![allow(non_upper_case_globals)]
use super::*;