From 7094a223af550fc95b039225bc5c67a53c7f0931 Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Tue, 20 Jun 2023 19:07:49 +0000 Subject: [PATCH] Bring `OptionAsAlt` back for macOS The correct handling of this setting requires to change the events we're getting from the macOS on the fly and call `interpretKeyEvents`, which could affect handling of the next events, meaning that we can't provide them on `KeyEvent`. --- CHANGELOG.md | 1 - examples/window_option_as_alt.rs | 75 +++++++++++++++++++++++++ src/platform/macos.rs | 51 +++++++++++++++++ src/platform_impl/macos/appkit/event.rs | 29 ++++++++++ src/platform_impl/macos/view.rs | 46 +++++++++++++-- src/platform_impl/macos/window.rs | 18 +++++- 6 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 examples/window_option_as_alt.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9229c5f2..ab27f2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,6 @@ And please only add new entries to the top of this list, right below the `# Unre portable) interpretations of a given key-press. - Add `KeyCodeExtScancode`, which lets you convert between raw keycodes and `KeyCode`. - - Remove `WindowExtMacOS::option_as_alt` and `WindowExtMacOS::set_option_as_alt`. - `ModifiersChanged` now uses dedicated `Modifiers` struct. - On Orbital, fix `ModifiersChanged` not being sent. - **Breaking:** `CursorIcon` is now used from the `cursor-icon` crate. diff --git a/examples/window_option_as_alt.rs b/examples/window_option_as_alt.rs new file mode 100644 index 00000000..fc2e4582 --- /dev/null +++ b/examples/window_option_as_alt.rs @@ -0,0 +1,75 @@ +#![allow(clippy::single_match)] + +#[cfg(target_os = "macos")] +use winit::platform::macos::{OptionAsAlt, WindowExtMacOS}; + +#[cfg(target_os = "macos")] +use winit::{ + event::ElementState, + event::{Event, MouseButton, WindowEvent}, + event_loop::EventLoop, + window::WindowBuilder, +}; + +#[cfg(target_os = "macos")] +#[path = "util/fill.rs"] +mod fill; + +/// Prints the keyboard events characters received when option_is_alt is true versus false. +/// A left mouse click will toggle option_is_alt. +#[cfg(target_os = "macos")] +fn main() { + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)) + .build(&event_loop) + .unwrap(); + + window.set_ime_allowed(true); + + let mut option_as_alt = window.option_as_alt(); + + event_loop.run(move |event, _, control_flow| { + control_flow.set_wait(); + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } if window_id == window.id() => control_flow.set_exit(), + Event::WindowEvent { event, .. } => match event { + WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + .. + } => { + option_as_alt = match option_as_alt { + OptionAsAlt::None => OptionAsAlt::OnlyLeft, + OptionAsAlt::OnlyLeft => OptionAsAlt::OnlyRight, + OptionAsAlt::OnlyRight => OptionAsAlt::Both, + OptionAsAlt::Both => OptionAsAlt::None, + }; + + println!("Received Mouse click, toggling option_as_alt to: {option_as_alt:?}"); + window.set_option_as_alt(option_as_alt); + } + WindowEvent::KeyboardInput { .. } => println!("KeyboardInput: {event:?}"), + _ => (), + }, + Event::MainEventsCleared => { + window.request_redraw(); + } + Event::RedrawRequested(_) => { + fill::fill_window(&window); + } + _ => (), + } + }); +} + +#[cfg(not(target_os = "macos"))] +fn main() { + println!("This example is only supported on MacOS"); +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index e4da83bd..18d4745d 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -56,6 +56,17 @@ pub trait WindowExtMacOS { /// Put the window in a state which indicates a file save is required. fn set_document_edited(&self, edited: bool); + + /// Set option as alt behavior as described in [`OptionAsAlt`]. + /// + /// This will ignore diacritical marks and accent characters from + /// being processed as received characters. Instead, the input + /// device's raw character will be placed in event queues with the + /// Alt modifier set. + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt); + + /// Getter for the [`WindowExtMacOS::set_option_as_alt`]. + fn option_as_alt(&self) -> OptionAsAlt; } impl WindowExtMacOS for Window { @@ -98,6 +109,16 @@ impl WindowExtMacOS for Window { fn set_document_edited(&self, edited: bool) { self.window.set_document_edited(edited) } + + #[inline] + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { + self.window.set_option_as_alt(option_as_alt) + } + + #[inline] + fn option_as_alt(&self) -> OptionAsAlt { + self.window.option_as_alt() + } } /// Corresponds to `NSApplicationActivationPolicy`. @@ -140,6 +161,10 @@ pub trait WindowBuilderExtMacOS { fn with_has_shadow(self, has_shadow: bool) -> WindowBuilder; /// Window accepts click-through mouse events. fn with_accepts_first_mouse(self, accepts_first_mouse: bool) -> WindowBuilder; + /// Set how the Option keys are interpreted. + /// + /// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set. + fn with_option_as_alt(self, option_as_alt: OptionAsAlt) -> WindowBuilder; } impl WindowBuilderExtMacOS for WindowBuilder { @@ -199,6 +224,12 @@ impl WindowBuilderExtMacOS for WindowBuilder { self.platform_specific.accepts_first_mouse = accepts_first_mouse; self } + + #[inline] + fn with_option_as_alt(mut self, option_as_alt: OptionAsAlt) -> WindowBuilder { + self.platform_specific.option_as_alt = option_as_alt; + self + } } pub trait EventLoopBuilderExtMacOS { @@ -309,3 +340,23 @@ impl EventLoopWindowTargetExtMacOS for EventLoopWindowTarget { self.p.hide_other_applications() } } + +/// Option as alt behavior. +/// +/// The default is `None`. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum OptionAsAlt { + /// The left `Option` key is treated as `Alt`. + OnlyLeft, + + /// The right `Option` key is treated as `Alt`. + OnlyRight, + + /// Both `Option` keys are treated as `Alt`. + Both, + + /// No special handling is applied for `Option` key. + #[default] + None, +} diff --git a/src/platform_impl/macos/appkit/event.rs b/src/platform_impl/macos/appkit/event.rs index e8992585..5952c43c 100644 --- a/src/platform_impl/macos/appkit/event.rs +++ b/src/platform_impl/macos/appkit/event.rs @@ -67,6 +67,35 @@ extern_methods!( } } + pub fn keyEventWithType( + type_: NSEventType, + location: NSPoint, + modifier_flags: NSEventModifierFlags, + timestamp: NSTimeInterval, + window_num: NSInteger, + context: Option<&NSObject>, + characters: &NSString, + characters_ignoring_modifiers: &NSString, + is_a_repeat: bool, + scancode: c_ushort, + ) -> Id { + unsafe { + msg_send_id![ + Self::class(), + keyEventWithType: type_, + location: location, + modifierFlags: modifier_flags, + timestamp: timestamp, + windowNumber: window_num, + context: context, + characters: characters, + charactersIgnoringModifiers: characters_ignoring_modifiers, + isARepeat: is_a_repeat, + keyCode: scancode, + ] + } + } + #[sel(locationInWindow)] pub fn locationInWindow(&self) -> NSPoint; diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index faecdd15..0ae81874 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -28,6 +28,7 @@ use crate::{ TouchPhase, WindowEvent, }, keyboard::{Key, KeyCode, KeyLocation, ModifiersState}, + platform::macos::{OptionAsAlt, WindowExtMacOS}, platform::scancode::KeyCodeExtScancode, platform_impl::platform::{ app_state::AppState, @@ -482,6 +483,7 @@ declare_class!( // Get the characters from the event. let old_ime_state = self.state.ime_state; self.state.forward_key_to_app = false; + let event = replace_event(event, self.window().option_as_alt()); // The `interpretKeyEvents` function might call // `setMarkedText`, `insertText`, and `doCommandBySelector`. @@ -500,7 +502,7 @@ declare_class!( } } - self.update_modifiers(event, false); + self.update_modifiers(&event, false); let had_ime_input = match self.state.ime_state { ImeState::Commited => { @@ -514,7 +516,7 @@ declare_class!( }; if !had_ime_input || self.state.forward_key_to_app { - let key_event = create_key_event(event, true, event.is_a_repeat(), None); + let key_event = create_key_event(&event, true, event.is_a_repeat(), None); self.queue_event(WindowEvent::KeyboardInput { device_id: DEVICE_ID, event: key_event, @@ -527,13 +529,14 @@ declare_class!( fn key_up(&mut self, event: &NSEvent) { trace_scope!("keyUp:"); - self.update_modifiers(event, false); + let event = replace_event(event, self.window().option_as_alt()); + self.update_modifiers(&event, false); // We want to send keyboard input when we are currently in the ground state. if matches!(self.state.ime_state, ImeState::Ground | ImeState::Disabled) { self.queue_event(WindowEvent::KeyboardInput { device_id: DEVICE_ID, - event: create_key_event(event, false, false, None), + event: create_key_event(&event, false, false, None), is_synthetic: false, }); } @@ -1038,3 +1041,38 @@ fn mouse_button(event: &NSEvent) -> MouseButton { n => MouseButton::Other(n as u16), } } + +// NOTE: to get option as alt working we need to rewrite events +// we're getting from the operating system, which makes it +// impossible to provide such events as extra in `KeyEvent`. +fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Id { + let ev_mods = event_mods(event).state; + let ignore_alt_characters = match option_as_alt { + OptionAsAlt::OnlyLeft if event.lalt_pressed() => true, + OptionAsAlt::OnlyRight if event.ralt_pressed() => true, + OptionAsAlt::Both if ev_mods.alt_key() => true, + _ => false, + } && !ev_mods.control_key() + && !ev_mods.super_key(); + + if ignore_alt_characters { + let ns_chars = event + .charactersIgnoringModifiers() + .expect("expected characters to be non-null"); + + NSEvent::keyEventWithType( + event.type_(), + event.locationInWindow(), + event.modifierFlags(), + event.timestamp(), + event.window_number(), + None, + &ns_chars, + &ns_chars, + event.is_a_repeat(), + event.key_code(), + ) + } else { + event.copy() + } +} diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 05aad7b1..2ba6acaa 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -21,7 +21,7 @@ use crate::{ error::{ExternalError, NotSupportedError, OsError as RootOsError}, event::WindowEvent, icon::Icon, - platform::macos::WindowExtMacOS, + platform::macos::{OptionAsAlt, WindowExtMacOS}, platform_impl::platform::{ app_state::AppState, appkit::NSWindowOrderingMode, @@ -85,6 +85,7 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub disallow_hidpi: bool, pub has_shadow: bool, pub accepts_first_mouse: bool, + pub option_as_alt: OptionAsAlt, } impl Default for PlatformSpecificWindowBuilderAttributes { @@ -100,6 +101,7 @@ impl Default for PlatformSpecificWindowBuilderAttributes { disallow_hidpi: false, has_shadow: true, accepts_first_mouse: true, + option_as_alt: Default::default(), } } } @@ -160,6 +162,8 @@ pub struct SharedState { /// The current resize incerments for the window content. pub(crate) resize_increments: NSSize, + /// The state of the `Option` as `Alt`. + pub(crate) option_as_alt: OptionAsAlt, } impl SharedState { @@ -369,6 +373,8 @@ impl WinitWindow { this.center(); } + this.set_option_as_alt(pl_attrs.option_as_alt); + Id::into_shared(this) }) }) @@ -1376,6 +1382,16 @@ impl WindowExtMacOS for WinitWindow { fn set_document_edited(&self, edited: bool) { self.setDocumentEdited(edited) } + + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { + let mut shared_state_lock = self.shared_state.lock().unwrap(); + shared_state_lock.option_as_alt = option_as_alt; + } + + fn option_as_alt(&self) -> OptionAsAlt { + let shared_state_lock = self.shared_state.lock().unwrap(); + shared_state_lock.option_as_alt + } } pub(super) fn get_ns_theme() -> Theme {