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](https://github.com/zed-industries/zed/blob/a890df1863ca939ee7f0ada6e629b6f83eb18bc5/crates/settings/src/key_equivalents.rs#L7). --------- Co-authored-by: Will <will@zed.dev>
This commit is contained in:
@@ -546,7 +546,6 @@ impl InputExample {
|
||||
|
||||
impl Render for InputExample {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let num_keystrokes = self.recent_keystrokes.len();
|
||||
div()
|
||||
.bg(rgb(0xaaaaaa))
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
@@ -561,7 +560,7 @@ impl Render for InputExample {
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(format!("Keystrokes: {}", num_keystrokes))
|
||||
.child(format!("Keyboard {}", cx.keyboard_layout()))
|
||||
.child(
|
||||
div()
|
||||
.border_1()
|
||||
@@ -607,6 +606,7 @@ fn main() {
|
||||
KeyBinding::new("end", End, None),
|
||||
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
||||
]);
|
||||
|
||||
let window = cx
|
||||
.open_window(
|
||||
WindowOptions {
|
||||
@@ -642,6 +642,13 @@ fn main() {
|
||||
.unwrap();
|
||||
})
|
||||
.detach();
|
||||
cx.on_keyboard_layout_change({
|
||||
move |cx| {
|
||||
window.update(cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
window
|
||||
.update(cx, |view, cx| {
|
||||
cx.focus_view(&view.text_input);
|
||||
|
||||
@@ -243,6 +243,7 @@ pub struct AppContext {
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
|
||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||
pub(crate) keyboard_layout: SharedString,
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
@@ -252,6 +253,7 @@ pub struct AppContext {
|
||||
// TypeId is the type of the event that the listener callback expects
|
||||
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
|
||||
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
|
||||
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
||||
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
||||
@@ -279,6 +281,7 @@ impl AppContext {
|
||||
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
let entities = EntityMap::new();
|
||||
let keyboard_layout = SharedString::from(platform.keyboard_layout());
|
||||
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
app: RefCell::new(AppContext {
|
||||
@@ -302,6 +305,7 @@ impl AppContext {
|
||||
window_handles: FxHashMap::default(),
|
||||
windows: SlotMap::with_key(),
|
||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||
keyboard_layout,
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
pending_effects: VecDeque::new(),
|
||||
pending_notifications: FxHashSet::default(),
|
||||
@@ -310,6 +314,7 @@ impl AppContext {
|
||||
event_listeners: SubscriberSet::new(),
|
||||
release_listeners: SubscriberSet::new(),
|
||||
keystroke_observers: SubscriberSet::new(),
|
||||
keyboard_layout_observers: SubscriberSet::new(),
|
||||
global_observers: SubscriberSet::new(),
|
||||
quit_observers: SubscriberSet::new(),
|
||||
layout_id_buffer: Default::default(),
|
||||
@@ -323,6 +328,19 @@ impl AppContext {
|
||||
|
||||
init_app_menus(platform.as_ref(), &mut app.borrow_mut());
|
||||
|
||||
platform.on_keyboard_layout_change(Box::new({
|
||||
let app = Rc::downgrade(&app);
|
||||
move || {
|
||||
if let Some(app) = app.upgrade() {
|
||||
let cx = &mut app.borrow_mut();
|
||||
cx.keyboard_layout = SharedString::from(cx.platform.keyboard_layout());
|
||||
cx.keyboard_layout_observers
|
||||
.clone()
|
||||
.retain(&(), move |callback| (callback)(cx));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_quit(Box::new({
|
||||
let cx = app.clone();
|
||||
move || {
|
||||
@@ -356,6 +374,27 @@ impl AppContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the id of the current keyboard layout
|
||||
pub fn keyboard_layout(&self) -> &SharedString {
|
||||
&self.keyboard_layout
|
||||
}
|
||||
|
||||
/// Invokes a handler when the current keyboard layout changes
|
||||
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
||||
where
|
||||
F: 'static + FnMut(&mut AppContext),
|
||||
{
|
||||
let (subscription, activate) = self.keyboard_layout_observers.insert(
|
||||
(),
|
||||
Box::new(move |cx| {
|
||||
callback(cx);
|
||||
true
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Gracefully quit the application via the platform's standard routine.
|
||||
pub fn quit(&self) {
|
||||
self.platform.quit();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{Action, KeyBindingContextPredicate, Keystroke};
|
||||
use anyhow::Result;
|
||||
use smallvec::SmallVec;
|
||||
@@ -22,22 +24,37 @@ impl Clone for KeyBinding {
|
||||
impl KeyBinding {
|
||||
/// Construct a new keybinding from the given data.
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
|
||||
Self::load(keystrokes, Box::new(action), context_predicate).unwrap()
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
|
||||
}
|
||||
|
||||
/// Load a keybinding from the given raw data.
|
||||
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
|
||||
pub fn load(
|
||||
keystrokes: &str,
|
||||
action: Box<dyn Action>,
|
||||
context: Option<&str>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
) -> Result<Self> {
|
||||
let context = if let Some(context) = context {
|
||||
Some(KeyBindingContextPredicate::parse(context)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let keystrokes = keystrokes
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
.map(Keystroke::parse)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
if let Some(equivalents) = key_equivalents {
|
||||
for keystroke in keystrokes.iter_mut() {
|
||||
if keystroke.key.chars().count() == 1 {
|
||||
if let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) {
|
||||
keystroke.key = key.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
keystrokes,
|
||||
action,
|
||||
|
||||
@@ -169,6 +169,7 @@ pub(crate) trait Platform: 'static {
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
@@ -180,6 +181,7 @@ pub(crate) trait Platform: 'static {
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
fn keyboard_layout(&self) -> String;
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
""
|
||||
|
||||
@@ -138,6 +138,14 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
self.with_common(|common| common.text_system.clone())
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> String {
|
||||
"unknown".into()
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {
|
||||
// todo(linux)
|
||||
}
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
|
||||
on_finish_launching();
|
||||
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -162,6 +162,12 @@ impl Platform for TestPlatform {
|
||||
self.text_system.clone()
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> String {
|
||||
"zed.keyboard.example".to_string()
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -197,6 +197,14 @@ impl Platform for WindowsPlatform {
|
||||
self.text_system.clone()
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> String {
|
||||
"unknown".into()
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {
|
||||
// todo(windows)
|
||||
}
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
||||
on_finish_launching();
|
||||
let vsync_event = unsafe { Owned::new(CreateEventW(None, false, false, None).unwrap()) };
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
use collections::HashMap;
|
||||
|
||||
// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range
|
||||
// without using option. This means that some of our built in keyboard shortcuts do not work
|
||||
// for those users.
|
||||
//
|
||||
// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
|
||||
// even if the mnemoic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
|
||||
//
|
||||
// For example, cmd-> is the "switch window" shortcut because the > key is right above tab.
|
||||
// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves
|
||||
// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position
|
||||
// as cmd-> on a QWERTY layout.
|
||||
//
|
||||
// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö
|
||||
// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard
|
||||
// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the
|
||||
// specific key moves)
|
||||
//
|
||||
// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every
|
||||
// possible key combination, and inspecting the UI to see what it rendered. So that's what we did...
|
||||
//
|
||||
// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the
|
||||
// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with:
|
||||
// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add'
|
||||
// From there I used multi-cursor to produce this match statement.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_key_equivalents(layout: &str) -> Option<HashMap<char, char>> {
|
||||
let (from, to) = match layout {
|
||||
"com.apple.keylayout.Welsh" => ("#", "£"),
|
||||
"com.apple.keylayout.Turkmen" => ("qc]Q`|[XV\\^v~Cx}{", "äçöÄžŞňÜÝş№ýŽÇüÖŇ"),
|
||||
"com.apple.keylayout.Turkish-QWERTY-PC" => (
|
||||
"$\\|`'[}^=.#{*+:/~;)(@<,&]>\"",
|
||||
"+,;<ığÜ&.ç^Ğ(:Ş*>ş=)'Öö/üÇI",
|
||||
),
|
||||
"com.apple.keylayout.Sami-PC" => (
|
||||
"}*x\"w[~^/@`]{|<)>W(\\X=Qq&':;",
|
||||
"Æ(čŊšøŽ&´\"žæØĐ;=:Š)đČ`Áá/ŋÅå",
|
||||
),
|
||||
"com.apple.keylayout.LatinAmerican" => {
|
||||
("[^~>`(<\\@{;*&/):]|='}\"", "{&>:<);¿\"[ñ(/'=Ñ}¡*´]¨")
|
||||
}
|
||||
"com.apple.keylayout.IrishExtended" => ("#", "£"),
|
||||
"com.apple.keylayout.Icelandic" => ("[}=:/'){(*&;^|`\"\\>]<~@", "æ´*Ð'ö=Æ)(/ð&Þ<Öþ:´;>\""),
|
||||
"com.apple.keylayout.German-DIN-2137" => {
|
||||
("}~/<^>{`:\\)&=[]@|;#'\"(*", "Ä>ß;&:Ö<Ü#=/*öä\"'ü§´`)(")
|
||||
}
|
||||
"com.apple.keylayout.FinnishSami-PC" => {
|
||||
(")=*\"\\[@{:>';/<|~(]}^`&", "=`(ˆ@ö\"ÖÅ:¨å´;*>)äÄ&</")
|
||||
}
|
||||
"com.apple.keylayout.FinnishExtended" => {
|
||||
("];{`:'*<~=/}\\|&[\"($^)>@", "äåÖ<Ũ(;>`´Ä'*/öˆ)€&=:\"")
|
||||
}
|
||||
"com.apple.keylayout.Faroese" => ("}\";/$>^@~`:&[*){|]=(\\<'", "ÐØæ´€:&\"><Æ/å(=Å*ð`)';ø"),
|
||||
"com.apple.keylayout.Croatian-PC" => {
|
||||
("{@~;<=>(&*['|]\":/}^`)\\", "Š\">č;*:)/(šćŽđĆČ'Đ&<=ž")
|
||||
}
|
||||
"com.apple.keylayout.Croatian" => ("{@;<~=>(&*['|]\":}^)\\`", "Š\"č;>*:)'(šćŽđĆČĐ&=ž<"),
|
||||
"com.apple.keylayout.Azeri" => (":{W?./\"[}<]|,>';w", "IÖÜ,ş.ƏöĞÇğ/çŞəıü"),
|
||||
"com.apple.keylayout.Albanian" => ("\\'~;:|<>`\"@", "ë@>çÇË;:<'\""),
|
||||
"com.apple.keylayout.SwissFrench" => (
|
||||
":@&'~^)$;\"][\\/#={!|*+`<(>}",
|
||||
"ü\"/^>&=çè`àé$'*¨ö+£(!<;):ä",
|
||||
),
|
||||
"com.apple.keylayout.Swedish" => ("(]\\\"~$`^{|/>*:;<)&=[}'@", ")ä'^>€<&Ö*´:(Åå;=/`öĨ\""),
|
||||
"com.apple.keylayout.Swedish-Pro" => {
|
||||
("/^*`'{|)$>&<[\\;(~\"}@]:=", "´&(<¨Ö*=€:/;ö'å)>^Ä\"äÅ`")
|
||||
}
|
||||
"com.apple.keylayout.Spanish" => ("|!\\<{[:;@`/~].'>}\"^", "\"¡'¿Ññº´!<.>;ç`Ç:¨/"),
|
||||
"com.apple.keylayout.Spanish-ISO" => (
|
||||
"|~`]/:)(<&^>*;#}\"{.\\['@",
|
||||
"\"><;.º=)¿/&Ç(´·not found¨Ñç'ñ`\"",
|
||||
),
|
||||
"com.apple.keylayout.Portuguese" => (")`/'^\"<];>[:{@}(&*=~", "=<'´&`;~º:çªÇ\"^)/(*>"),
|
||||
"com.apple.keylayout.Italian" => (
|
||||
"*7};8:!5%(1&4]^\\6)32>.</0|$,'{[`\"~9#@",
|
||||
"8)*ò£!1ç59&7($6§è0'\"/:.,é°4;ù^ì<%>à32",
|
||||
),
|
||||
"com.apple.keylayout.Italian-Pro" => {
|
||||
("/:@[]'\\=){;|#<\"(*^&`}>~", "'é\"òàìù*=çè§£;^)(&/<°:>")
|
||||
}
|
||||
"com.apple.keylayout.Irish" => ("#", "£"),
|
||||
"com.apple.keylayout.German" => ("=`#'}:)/\"^&]*{;|[<(>~@\\", "*<§´ÄÜ=ß`&/ä(Öü'ö;):>\"#"),
|
||||
"com.apple.keylayout.French" => (
|
||||
"*}7;8:!5%(1&4]\\^6)32>.</0|${'[`\"~9#@",
|
||||
"8*è)!°1(59&7'$`6§0\"é/;.:à£4¨ù^<%>ç32",
|
||||
),
|
||||
"com.apple.keylayout.French-numerical" => (
|
||||
"|!52;][>&@\"%'{)<~7.1/^(}*8#0$9`6\\3:4",
|
||||
"£1(é)$^/72%5ù¨0.>è;&:69*8!3à4ç<§`\"°'",
|
||||
),
|
||||
"com.apple.keylayout.French-PC" => (
|
||||
"!&\"_$}/72>8]#:31)*<%4;6\\-{['@(0|5.`9~^",
|
||||
"17%°4£:èé/_$3§\"&08.5'!-*)¨^ù29àμ(;<ç>6",
|
||||
),
|
||||
"com.apple.keylayout.Finnish" => ("/^*`)'{|$>&<[\\~;(\"}@]:=", "´&(<=¨Ö*€:/;ö'>å)^Ä\"äÅ`"),
|
||||
"com.apple.keylayout.Danish" => ("=[;'`{}|>]*^(&@~)<\\/$\":", "`æå¨<ÆØ*:ø(&)/\">=;'´€^Å"),
|
||||
"com.apple.keylayout.Canadian-CSA" => ("\\?']/><[{}|~`\"", "àÉèçé\"'^¨ÇÀÙùÈ"),
|
||||
"com.apple.keylayout.British" => ("#", "£"),
|
||||
"com.apple.keylayout.Brazilian-ABNT2" => ("\"|~?`'/^\\", "`^\"Ç'´ç¨~"),
|
||||
"com.apple.keylayout.Belgian" => (
|
||||
"`3/*<\\8>7#&96@);024(|'1\":$[~5.%^}]{!",
|
||||
"<\":8.`!/è37ç§20)àé'9£ù&%°4^>(;56*$¨1",
|
||||
),
|
||||
"com.apple.keylayout.Austrian" => ("/^*`'{|)>&<[\\;(~\"}@]:=#", "ß&(<´Ö'=:/;ö#ü)>`Ä\"äÜ*§"),
|
||||
"com.apple.keylayout.Slovak-QWERTY" => (
|
||||
"):9;63'\"]^/+@~>`?<!#5&${2}%*18(704[",
|
||||
"0\"íôžš§!ä6'%2Ň:ňˇ?13ť74ÚľÄ58+á9ýéčú",
|
||||
),
|
||||
"com.apple.keylayout.Slovak" => (
|
||||
"!$`10&:#4^*~{%5')}6/\"[8]97?;<@23>(+",
|
||||
"14ň+é7\"3č68ŇÚ5ť§0Äž'!úáäíýˇô?2ľš:9%",
|
||||
),
|
||||
"com.apple.keylayout.Polish" => (
|
||||
"&)|?,%:;^}]_{!+#(*`/[~<\"$.>'@=\\",
|
||||
":\"$Ż.+Łł=)(ćź§]!/_<żó>śę?,ńą%[;",
|
||||
),
|
||||
"com.apple.keylayout.Lithuanian" => ("+#&=!%1*@73^584$26", "ŽĘŲžĄĮąŪČųęŠįūėĖčš"),
|
||||
"com.apple.keylayout.Hungarian" => (
|
||||
"}(*@\"{=/|;>'[`<~\\!$&0#:]^)+",
|
||||
"Ú)(\"ÁŐóüŰé:áőíÜÍű'!=ö+Éú/ÖÓ",
|
||||
),
|
||||
"com.apple.keylayout.Hungarian-QWERTY" => (
|
||||
"=]#>@/&<`0')~(\\!:*;$\"+^{|}[",
|
||||
"óú+:\"ü=ÜíöáÖÍ)ű'É(é!ÁÓ/ŐŰÚő",
|
||||
),
|
||||
"com.apple.keylayout.Czech-QWERTY" => (
|
||||
"9>0[2()\"}@]46%5;#8{*7^~+!3?&'<$/1`:",
|
||||
"í:éúě90!(2)čž5řů3áÚ8ý6`%1šˇ7§?4'+¨\"",
|
||||
),
|
||||
"com.apple.keylayout.Maltese" => ("[`}{#]~", "ġżĦĠ£ħŻ"),
|
||||
"com.apple.keylayout.Turkish" => (
|
||||
"|}(#>&^-/`$%@]~*,[\"<_.{:'\\)",
|
||||
"ÜI%\"Ç)/ş.<'(*ı>_öğ-ÖŞçĞ$,ü:",
|
||||
),
|
||||
"com.apple.keylayout.Turkish-Standard" => {
|
||||
("|}(#>=&^`@]~*,;[\"<.{:'\\)", "ÜI)^;*'&ö\"ıÖ(.çğŞ:,ĞÇşü=")
|
||||
}
|
||||
"com.apple.keylayout.NorwegianSami-PC" => {
|
||||
("\"}~<`&>':{@*^|\\)=([]/;", "ˆÆ>;</:¨ÅØ\"(&*@=`)øæ´å")
|
||||
}
|
||||
"com.apple.keylayout.Serbian-Latin" => {
|
||||
(";\\@>&'<]\"|(=}^)`[~:*{", "čž\":'ć;đĆŽ)*Đ&=<š>Č(Š")
|
||||
}
|
||||
"com.apple.keylayout.Slovenian" => ("]`^@)&\":'*=<{;}(~>\\|[", "đ<&\"='ĆČć(*;ŠčĐ)>:žŽš"),
|
||||
"com.apple.keylayout.SwedishSami-PC" => {
|
||||
("@=<^|`>){'&\"}]~[/:*\\(;", "\"`;&*<:=Ö¨/ˆÄä>ö´Å(@)å")
|
||||
}
|
||||
"com.apple.keylayout.SwissGerman" => (
|
||||
"={#:\\}!(+]/<\";$'`*[>&^~@)|",
|
||||
"¨é*è$à+)!ä';`üç^<(ö:/&>\"=£",
|
||||
),
|
||||
"com.apple.keylayout.Hawaiian" => ("'", "ʻ"),
|
||||
"com.apple.keylayout.NorthernSami" => (
|
||||
":/[<{X\"wQx\\(;~>W}`*@])'^|=q&",
|
||||
"Å´ø;ØČŊšÁčđ)åŽ:ŠÆž(\"æ=ŋ&Đ`á/",
|
||||
),
|
||||
"com.apple.keylayout.USInternational-PC" => ("^~", "ˆ˜"),
|
||||
"com.apple.keylayout.NorwegianExtended" => ("^~", "ˆ˜"),
|
||||
"com.apple.keylayout.Norwegian" => ("`'~\"\\*|=/@)[:}&><]{(^;", "<¨>^@(*`´\"=øÅÆ/:;æØ)&å"),
|
||||
"com.apple.keylayout.ABC-QWERTZ" => {
|
||||
("\"}~<`>'&#:{@*^|\\)=(]/;[", "`Ä>;<:´/§ÜÖ\"(&'#=*)äßüö")
|
||||
}
|
||||
"com.apple.keylayout.ABC-AZERTY" => (
|
||||
">[$61%@7|)&8\":}593(.4^<!{`2]\\#;~*/'0",
|
||||
"/^4§&52è£07!%°*(ç\"9;'6.1¨<é$`3)>8:ùà",
|
||||
),
|
||||
"com.apple.keylayout.Czech" => (
|
||||
"(7*#193620?/{)@~!$8+;:%4\">`^]&5}[<'",
|
||||
"9ý83+íšžěéˇ'Ú02`14á%ů\"5č!:¨6)7ř(ú?§",
|
||||
),
|
||||
"com.apple.keylayout.Brazilian-Pro" => ("^~", "ˆ˜"),
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
debug_assert!(from.chars().count() == to.chars().count());
|
||||
|
||||
Some(HashMap::from_iter(from.chars().zip(to.chars())))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn get_key_equivalents(_layout: &str) -> Option<HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
@@ -19,6 +19,8 @@ pub struct KeymapFile(Vec<KeymapBlock>);
|
||||
pub struct KeymapBlock {
|
||||
#[serde(default)]
|
||||
context: Option<String>,
|
||||
#[serde(default)]
|
||||
use_layout_keys: Option<bool>,
|
||||
bindings: BTreeMap<String, KeymapAction>,
|
||||
}
|
||||
|
||||
@@ -74,7 +76,14 @@ impl KeymapFile {
|
||||
}
|
||||
|
||||
pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
|
||||
for KeymapBlock { context, bindings } in self.0 {
|
||||
let key_equivalents = crate::key_equivalents::get_key_equivalents(&cx.keyboard_layout());
|
||||
|
||||
for KeymapBlock {
|
||||
context,
|
||||
use_layout_keys,
|
||||
bindings,
|
||||
} in self.0
|
||||
{
|
||||
let bindings = bindings
|
||||
.into_iter()
|
||||
.filter_map(|(keystroke, action)| {
|
||||
@@ -110,7 +119,18 @@ impl KeymapFile {
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
.map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
|
||||
.map(|action| {
|
||||
KeyBinding::load(
|
||||
&keystroke,
|
||||
action,
|
||||
context.as_deref(),
|
||||
if use_layout_keys.unwrap_or_default() {
|
||||
None
|
||||
} else {
|
||||
key_equivalents.as_ref()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod editable_setting_control;
|
||||
mod json_schema;
|
||||
mod key_equivalents;
|
||||
mod keymap_file;
|
||||
mod settings_file;
|
||||
mod settings_store;
|
||||
|
||||
@@ -4730,6 +4730,7 @@ impl Render for Workspace {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let mut context = KeyContext::new_with_defaults();
|
||||
context.add("Workspace");
|
||||
context.set("keyboard_layout", cx.keyboard_layout().clone());
|
||||
let centered_layout = self.centered_layout
|
||||
&& self.center.panes().len() == 1
|
||||
&& self.active_item(cx).is_some();
|
||||
|
||||
@@ -808,6 +808,7 @@ pub fn handle_keymap_file_changes(
|
||||
VimModeSetting::register(cx);
|
||||
|
||||
let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
|
||||
let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
|
||||
let mut old_base_keymap = *BaseKeymap::get_global(cx);
|
||||
let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
@@ -822,6 +823,11 @@ pub fn handle_keymap_file_changes(
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.on_keyboard_layout_change(move |_| {
|
||||
keyboard_layout_tx.unbounded_send(()).ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
load_default_keymap(cx);
|
||||
|
||||
cx.spawn(move |cx| async move {
|
||||
@@ -829,6 +835,7 @@ pub fn handle_keymap_file_changes(
|
||||
loop {
|
||||
select_biased! {
|
||||
_ = base_keymap_rx.next() => {}
|
||||
_ = keyboard_layout_rx.next() => {}
|
||||
user_keymap_content = user_keymap_file_rx.next() => {
|
||||
if let Some(user_keymap_content) = user_keymap_content {
|
||||
match KeymapFile::parse(&user_keymap_content) {
|
||||
@@ -854,7 +861,7 @@ fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
|
||||
load_default_keymap(cx);
|
||||
keymap_content.clone().add_to_cx(cx).log_err();
|
||||
cx.set_menus(app_menus());
|
||||
cx.set_dock_menu(vec![MenuItem::action("New Window", workspace::NewWindow)])
|
||||
cx.set_dock_menu(vec![MenuItem::action("New Window", workspace::NewWindow)]);
|
||||
}
|
||||
|
||||
pub fn load_default_keymap(cx: &mut AppContext) {
|
||||
|
||||
Reference in New Issue
Block a user