From 180a4c7a16459cec8abc0890945bed7410c909d7 Mon Sep 17 00:00:00 2001 From: Jack Wright <56345+ayax79@users.noreply.github.com> Date: Tue, 31 Jan 2023 01:35:49 -0800 Subject: [PATCH] Add `WindowExtMacOS::{set_,}option_as_alt` This adds an ability to control left and right `Option` keys to be treated as `Alt`, thus not producing diacritical marks. Co-authored-by: Kirill Chibisov --- .gitignore | 2 +- CHANGELOG.md | 1 + examples/window_option_as_alt.rs | 67 +++++++++++++++++++++++++ src/platform/macos.rs | 60 +++++++++++++++++++++- src/platform_impl/macos/appkit/event.rs | 52 +++++++++++++++++++ src/platform_impl/macos/view.rs | 41 +++++++++++++-- src/platform_impl/macos/window.rs | 19 ++++++- 7 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 examples/window_option_as_alt.rs diff --git a/.gitignore b/.gitignore index 5db37e16..ebed8e36 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ rls/ *.ts *.js #*# -.DS_Store \ No newline at end of file +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index d72624b6..1d05fb43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- On macOS, added `WindowExtMacOS::option_as_alt` and `WindowExtMacOS::set_option_as_alt`. - On Windows, fix window size for maximized, undecorated windows. - On Windows and macOS, add `WindowBuilder::with_active`. - Add `Window::is_minimized`. diff --git a/examples/window_option_as_alt.rs b/examples/window_option_as_alt.rs new file mode 100644 index 00000000..b7d288d5 --- /dev/null +++ b/examples/window_option_as_alt.rs @@ -0,0 +1,67 @@ +#![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, +}; + +/// 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(); + + 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::ReceivedCharacter(c) => println!("ReceivedCharacter: {c:?}"), + WindowEvent::KeyboardInput { .. } => println!("KeyboardInput: {event:?}"), + _ => (), + }, + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), + } + }); +} + +#[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 c2be433f..586f4320 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,6 +1,7 @@ -use objc2::rc::Id; use std::os::raw::c_void; +use objc2::rc::Id; + use crate::{ event_loop::{EventLoopBuilder, EventLoopWindowTarget}, monitor::MonitorHandle, @@ -55,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 { @@ -97,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`. @@ -142,6 +164,11 @@ 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 whether the `OptionAsAlt` key is interpreted as the `Alt` modifier. + /// + /// 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 { @@ -201,6 +228,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 { @@ -311,3 +344,28 @@ impl EventLoopWindowTargetExtMacOS for EventLoopWindowTarget { self.p.hide_other_applications() } } + +/// Option as alt behavior. +/// +/// The default is `None`. +#[derive(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. + None, +} + +impl Default for OptionAsAlt { + fn default() -> Self { + OptionAsAlt::None + } +} diff --git a/src/platform_impl/macos/appkit/event.rs b/src/platform_impl/macos/appkit/event.rs index a9521576..7924f420 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; @@ -123,6 +152,15 @@ extern_methods!( #[sel(stage)] pub fn stage(&self) -> NSInteger; + #[sel(isARepeat)] + pub fn is_a_repeat(&self) -> bool; + + #[sel(windowNumber)] + pub fn window_number(&self) -> NSInteger; + + #[sel(timestamp)] + pub fn timestamp(&self) -> NSTimeInterval; + pub fn characters(&self) -> Option> { unsafe { msg_send_id![self, characters] } } @@ -130,6 +168,16 @@ extern_methods!( pub fn charactersIgnoringModifiers(&self) -> Option> { unsafe { msg_send_id![self, charactersIgnoringModifiers] } } + + pub fn lalt_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICELALTKEYMASK != 0 + } + + pub fn ralt_pressed(&self) -> bool { + let raw_modifiers = self.modifierFlags().bits() as u32; + raw_modifiers & NX_DEVICERALTKEYMASK != 0 + } } ); @@ -138,6 +186,10 @@ unsafe impl NSCopying for NSEvent { type Output = NSEvent; } +// The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259 +const NX_DEVICELALTKEYMASK: u32 = 0x00000020; +const NX_DEVICERALTKEYMASK: u32 = 0x00000040; + bitflags! { pub struct NSEventModifierFlags: NSUInteger { const NSAlphaShiftKeyMask = 1 << 16; diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index c51f049f..171a231d 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -15,6 +15,7 @@ use super::appkit::{ NSApp, NSCursor, NSEvent, NSEventModifierFlags, NSEventPhase, NSResponder, NSTrackingRectTag, NSView, }; +use crate::platform::macos::{OptionAsAlt, WindowExtMacOS}; use crate::{ dpi::{LogicalPosition, LogicalSize}, event::{ @@ -463,7 +464,17 @@ declare_class!( } let was_in_preedit = self.state.ime_state == ImeState::Preedit; - let characters = get_characters(event, false); + // Get the characters from the event. + let ev_mods = event_mods(event); + let ignore_alt_characters = match self.window().option_as_alt() { + OptionAsAlt::OnlyLeft if event.lalt_pressed() => true, + OptionAsAlt::OnlyRight if event.ralt_pressed() => true, + OptionAsAlt::Both if ev_mods.alt() => true, + _ => false, + } && !ev_mods.ctrl() + && !ev_mods.logo(); + + let characters = get_characters(event, ignore_alt_characters); self.state.forward_key_to_app = false; // The `interpretKeyEvents` function might call @@ -474,7 +485,13 @@ declare_class!( // is not handled by IME and should be handled by the application) let mut text_commited = false; if self.state.ime_allowed { - let events_for_nsview = NSArray::from_slice(&[event.copy()]); + let new_event = if ignore_alt_characters { + replace_event_chars(event, &characters) + } else { + event.copy() + }; + + let events_for_nsview = NSArray::from_slice(&[new_event]); unsafe { self.interpretKeyEvents(&events_for_nsview) }; // If the text was commited we must treat the next keyboard event as IME related. @@ -501,7 +518,7 @@ declare_class!( state: ElementState::Pressed, scancode, virtual_keycode, - modifiers: event_mods(event), + modifiers: ev_mods, }, is_synthetic: false, }); @@ -982,3 +999,21 @@ fn mouse_button(event: &NSEvent) -> MouseButton { n => MouseButton::Other(n as u16), } } + +fn replace_event_chars(event: &NSEvent, characters: &str) -> Id { + let ns_chars = NSString::from_str(characters); + let chars_ignoring_mods = event.charactersIgnoringModifiers().unwrap(); + + NSEvent::keyEventWithType( + event.type_(), + event.locationInWindow(), + event.modifierFlags(), + event.timestamp(), + event.window_number(), + None, + &ns_chars, + &chars_ignoring_mods, + event.is_a_repeat(), + event.scancode(), + ) +} diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 68f12508..a5ff9532 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(), } } } @@ -157,6 +159,9 @@ pub struct SharedState { /// transitioning back to borderless fullscreen. save_presentation_opts: Option, pub current_theme: Option, + + /// The state of the `Option` as `Alt`. + pub(crate) option_as_alt: OptionAsAlt, } impl SharedState { @@ -368,6 +373,8 @@ impl WinitWindow { this.center(); } + this.set_option_as_alt(pl_attrs.option_as_alt); + Id::into_shared(this) }) }) @@ -1349,6 +1356,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 {