use super::{
error::PlatformResult,
utils::{superclass, to_nsstring},
};
use crate::{
shell::{
api_model::{Accelerator, CheckStatus, Menu, MenuItem, MenuItemRole, MenuRole},
platform::platform_impl::utils::from_nsstring,
Context, Handle, MenuDelegate, MenuHandle, MenuManager,
},
util::{update_diff, DiffResult, LateRefCell},
};
use cocoa::{
appkit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem},
base::{id, nil, NO, YES},
foundation::{NSInteger, NSUInteger},
};
use objc::{
class,
declare::ClassDecl,
msg_send,
rc::StrongPtr,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use once_cell::sync::Lazy;
use std::{
cell::RefCell,
collections::HashMap,
ffi::c_void,
fmt::Write,
hash::Hash,
mem::ManuallyDrop,
rc::{Rc, Weak},
};
struct StrongPtrWrapper(StrongPtr);
impl PartialEq for StrongPtrWrapper {
fn eq(&self, other: &Self) -> bool {
*self.0 == *other.0
}
}
impl Eq for StrongPtrWrapper {}
impl Hash for StrongPtrWrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(*self.0).hash(state);
}
}
pub struct PlatformMenuManager {
context: Context,
weak_self: LateRefCell<Weak<PlatformMenuManager>>,
app_menu: RefCell<Option<Rc<PlatformMenu>>>,
window_menus: RefCell<HashMap<StrongPtrWrapper, Rc<PlatformMenu>>>,
update_handle: RefCell<Option<Handle>>,
last_key_window: RefCell<StrongPtr>,
}
impl PlatformMenuManager {
pub fn new(context: Context) -> Self {
Self {
context,
weak_self: LateRefCell::new(),
app_menu: RefCell::new(None),
window_menus: RefCell::new(HashMap::new()),
update_handle: RefCell::new(None),
last_key_window: RefCell::new(unsafe { StrongPtr::new(nil) }),
}
}
pub(crate) fn assign_weak_self(&self, weak_self: Weak<PlatformMenuManager>) {
self.weak_self.set(weak_self);
}
unsafe fn key_window(&self) -> StrongPtr {
let app = NSApplication::sharedApplication(nil);
let mut key = StrongPtr::retain(msg_send![app, keyWindow]);
if *key != nil {
let key_class: id = msg_send![*key, className];
if from_nsstring(key_class) != "IMFlutterWindow" {
key = StrongPtr::new(nil);
}
}
if *key == nil {
key = self.last_key_window.borrow().clone();
}
self.last_key_window.replace(key.clone());
key
}
fn update_menu(&self) {
unsafe {
let mut menu = self.app_menu.borrow().clone();
let app = NSApplication::sharedApplication(nil);
let key = self.key_window();
if *key != nil {
menu = self
.window_menus
.borrow()
.get(&StrongPtrWrapper(key))
.cloned()
.or(menu);
}
match menu {
Some(menu) => {
let current: id = msg_send![app, mainMenu];
if current != *menu.menu {
menu.set_as_app_menu();
}
}
None => {
let () = msg_send![app, setMainMenu: nil];
}
}
}
}
fn schedule_update(&self) {
let weak_self = self.weak_self.borrow().clone();
if let Some(context) = self.context.get() {
let callback = context.run_loop.borrow().schedule_now(move || {
if let Some(s) = weak_self.upgrade() {
s.update_menu();
}
});
self.update_handle.borrow_mut().replace(callback);
}
}
pub fn set_app_menu(&self, menu: Option<Rc<PlatformMenu>>) -> PlatformResult<()> {
match menu {
Some(menu) => {
self.app_menu.borrow_mut().replace(menu);
}
None => {
self.app_menu.borrow_mut().take();
}
}
self.schedule_update();
Ok(())
}
pub fn set_menu_for_window(&self, window: StrongPtr, menu: Option<Rc<PlatformMenu>>) {
match menu {
Some(menu) => {
self.window_menus
.borrow_mut()
.insert(StrongPtrWrapper(window), menu);
}
None => {
self.window_menus
.borrow_mut()
.remove(&StrongPtrWrapper(window));
}
}
self.schedule_update();
}
pub fn window_will_close(&self, window: StrongPtr) {
if **self.last_key_window.borrow() == *window {
self.last_key_window.replace(unsafe { StrongPtr::new(nil) });
}
self.window_menus
.borrow_mut()
.remove(&StrongPtrWrapper(window));
self.schedule_update();
}
pub fn window_did_become_active(&self, _window: StrongPtr) {
self.schedule_update();
}
}
pub struct PlatformMenu {
handle: MenuHandle,
pub(super) menu: StrongPtr,
previous_menu: RefCell<Menu>,
id_to_menu_item: RefCell<HashMap<i64, StrongPtr>>,
target: StrongPtr,
weak_self: LateRefCell<Weak<PlatformMenu>>,
delegate: Weak<RefCell<dyn MenuDelegate>>,
}
const ITEM_TAG: NSInteger = 9999;
impl PlatformMenu {
pub fn new(
_context: Context,
handle: MenuHandle,
delegate: Weak<RefCell<dyn MenuDelegate>>,
) -> Self {
unsafe {
let menu: id = NSMenu::alloc(nil).initWithTitle_(*to_nsstring(""));
let () = msg_send![menu, setAutoenablesItems: NO];
let target: id = msg_send![*MENU_ITEM_TARGET_CLASS, new];
let target = StrongPtr::new(target);
let () = msg_send![menu, setDelegate:*target];
Self {
handle,
menu: StrongPtr::new(menu),
previous_menu: RefCell::new(Default::default()),
id_to_menu_item: RefCell::new(HashMap::new()),
target,
weak_self: LateRefCell::new(),
delegate,
}
}
}
pub fn assign_weak_self(&self, weak: Weak<PlatformMenu>) {
self.weak_self.set(weak.clone());
unsafe {
let state_ptr = weak.into_raw() as *mut c_void;
(**self.target).set_ivar("imState", state_ptr);
}
}
pub fn update_from_menu(&self, menu: Menu, manager: &MenuManager) -> PlatformResult<()> {
let mut previous_menu = self.previous_menu.borrow_mut();
let mut diff = update_diff(&previous_menu.items, &menu.items, |a, b| {
Self::can_update(a, b)
});
diff.retain(|res| {
match res {
DiffResult::Remove(res) => {
let item = self.id_to_menu_item.borrow_mut().remove(&res.id);
if let Some(item) = item {
unsafe {
let () = msg_send![*item, setMenu: nil];
let () = msg_send![*self.menu, removeItem:*item];
}
}
false
}
_ => true,
}
});
for (i, d) in diff.iter().enumerate() {
match d {
DiffResult::Remove(_) => {
panic!("Should have been already removed.")
}
DiffResult::Keep(_, _) => {
}
DiffResult::Update(old, new) => {
let item = self
.id_to_menu_item
.borrow_mut()
.remove(&old.id)
.unwrap()
.clone();
self.id_to_menu_item
.borrow_mut()
.insert(new.id, item.clone());
self.update_menu_item(*item, new, manager);
}
DiffResult::Insert(item) => {
let menu_item = self.create_menu_item(item, manager);
self.id_to_menu_item
.borrow_mut()
.insert(item.id, menu_item.clone());
unsafe { msg_send![*self.menu, insertItem:*menu_item atIndex:i as NSInteger] }
}
}
}
*previous_menu = menu;
assert!(
previous_menu.items.len() == self.id_to_menu_item.borrow().len(),
"Array length mismatch"
);
Ok(())
}
fn prepare_for_app_menu(&self) {
match self.previous_menu.borrow().role {
Some(MenuRole::Window) => unsafe {
let items: NSUInteger = msg_send![*self.menu, numberOfItems];
for i in (0..items).rev() {
let item: id = msg_send![*self.menu, itemAtIndex: i];
let tag: NSInteger = msg_send![item, tag];
if tag != ITEM_TAG {
let () = msg_send![*self.menu, removeItemAtIndex: i];
}
}
let app = NSApplication::sharedApplication(nil);
NSApplication::setWindowsMenu_(app, *self.menu);
let () = msg_send![*self.menu, setAutoenablesItems: YES];
},
Some(MenuRole::Services) => unsafe {
let app = NSApplication::sharedApplication(nil);
NSApplication::setServicesMenu_(app, *self.menu);
},
None => {}
};
let children: Vec<MenuHandle> = self
.previous_menu
.borrow()
.items
.iter()
.filter_map(|f| f.submenu)
.collect();
if let Some(delegate) = self.delegate.upgrade() {
for c in children {
let menu = delegate.borrow().get_platform_menu(c);
if let Ok(menu) = menu {
menu.prepare_for_app_menu();
}
}
}
}
fn set_as_app_menu(&self) {
unsafe {
let app = NSApplication::sharedApplication(nil);
let () = msg_send![app, setWindowsMenu: nil];
self.prepare_for_app_menu();
let () = msg_send![app, setMainMenu: *self.menu];
}
}
fn can_update(old_item: &MenuItem, new_item: &MenuItem) -> bool {
old_item.separator == new_item.separator
}
fn update_menu_item(&self, item: id, menu_item: &MenuItem, menu_manager: &MenuManager) {
if menu_item.separator {
return;
}
unsafe {
match &menu_item.role {
Some(role) => {
self.update_from_role(item, &menu_item.title, role.clone());
}
None => {
self.update_from_menu_item(item, menu_item, menu_manager);
}
}
}
}
unsafe fn update_from_role(&self, item: id, title: &str, role: MenuItemRole) {
let () = msg_send![item, setTitle:*to_nsstring(&remove_mnemonics(title))];
let () = msg_send![item, setTarget: nil];
match role {
MenuItemRole::About => {
let () = msg_send![item, setAction: sel!(orderFrontStandardAboutPanel:)];
}
MenuItemRole::Hide => {
let () = msg_send![item, setAction: sel!(hide:)];
let () = msg_send![item, setKeyEquivalent: to_nsstring("h")];
let () = msg_send![
item,
setKeyEquivalentModifierMask: NSEventModifierFlags::NSCommandKeyMask
];
}
MenuItemRole::HideOtherApplications => {
let () = msg_send![item, setAction: sel!(hideOtherApplications:)];
let () = msg_send![item, setKeyEquivalent: to_nsstring("h")];
let () = msg_send![
item,
setKeyEquivalentModifierMask: NSEventModifierFlags::NSCommandKeyMask
| NSEventModifierFlags::NSShiftKeyMask
];
}
MenuItemRole::ShowAll => {
let () = msg_send![item, setAction: sel!(unhideAllApplications:)];
}
MenuItemRole::QuitApplication => {
let () = msg_send![item, setAction: sel!(terminate:)];
let () = msg_send![item, setKeyEquivalent: to_nsstring("q")];
let () = msg_send![
item,
setKeyEquivalentModifierMask: NSEventModifierFlags::NSCommandKeyMask
];
}
MenuItemRole::MinimizeWindow => {
let () = msg_send![item, setAction: sel!(performMiniaturize:)];
let () = msg_send![item, setKeyEquivalent: to_nsstring("m")];
let () = msg_send![
item,
setKeyEquivalentModifierMask: NSEventModifierFlags::NSCommandKeyMask
];
}
MenuItemRole::ZoomWindow => {
let () = msg_send![item, setAction: sel!(performZoom:)];
}
MenuItemRole::BringAllToFront => {
let () = msg_send![item, setAction: sel!(arrangeInFront:)];
}
}
}
fn accelerator_label_to_string(&self, accelerator: &Accelerator) -> String {
let label = accelerator.label.to_lowercase();
let value = match label.as_str() {
"f1" => 0xF704,
"f2" => 0xF705,
"f3" => 0xF706,
"f4" => 0xF707,
"f5" => 0xF708,
"f6" => 0xF709,
"f7" => 0xF70A,
"f8" => 0xF70B,
"f9" => 0xF70C,
"f10" => 0xF70D,
"f11" => 0xF70E,
"f12" => 0xF70F,
"home" => 0xF729,
"end" => 0xF72B,
"insert" => 0xF727,
"delete" => 0xF728,
"backspace" => 0x0008,
"page up" => 0xF72C,
"page down" => 0xF72D,
"space" => 0x0020,
"tab" => 0x0009,
"enter" => 0x000d,
"arrow up" => 0xF700,
"arrow down" => 0xF701,
"arrow left" => 0xF702,
"arrow right" => 0xF703,
_ => label.chars().next().unwrap_or(0 as char) as u32,
};
let mut res = String::new();
if value > 0 {
res.push(std::char::from_u32(value).unwrap());
}
res
}
fn accelerator_label_to_modifier_flags(
&self,
accelerator: &Accelerator,
) -> NSEventModifierFlags {
let mut res = NSEventModifierFlags::empty();
if accelerator.alt {
res |= NSEventModifierFlags::NSAlternateKeyMask;
}
if accelerator.meta {
res |= NSEventModifierFlags::NSCommandKeyMask;
}
if accelerator.control {
res |= NSEventModifierFlags::NSControlKeyMask;
}
if accelerator.shift {
res |= NSEventModifierFlags::NSShiftKeyMask;
}
res
}
unsafe fn update_from_menu_item(
&self,
item: id,
menu_item: &MenuItem,
menu_manager: &MenuManager,
) {
let menu_item_title = to_nsstring(&remove_mnemonics(&menu_item.title));
if let Some(submenu) = menu_item
.submenu
.and_then(|s| menu_manager.get_platform_menu(s).ok())
{
let () = msg_send![item, setSubmenu:*submenu.menu];
let () = msg_send![*submenu.menu, setTitle:*menu_item_title];
let () = msg_send![item, setTarget: nil];
let () = msg_send![item, setAction: nil];
} else {
let () = msg_send![item, setSubmenu: nil];
let () = msg_send![item, setTarget: *self.target];
let () = msg_send![item, setAction: sel!(onAction:)];
if let Some(accelerator) = &menu_item.accelerator {
let str = self.accelerator_label_to_string(accelerator);
if !str.is_empty() {
let () = msg_send![item, setKeyEquivalent: to_nsstring(&str)];
let () = msg_send![item, setKeyEquivalentModifierMask:
self.accelerator_label_to_modifier_flags(accelerator)];
}
}
}
let () = msg_send![item, setTitle:*menu_item_title];
let () = msg_send![item, setEnabled:menu_item.enabled];
let state: NSInteger = {
match menu_item.check_status == CheckStatus::CheckOn
|| menu_item.check_status == CheckStatus::RadioOn
{
true => 1,
false => 0,
}
};
let () = msg_send![item, setState: state];
let number: id = msg_send![class!(NSNumber), numberWithLongLong:menu_item.id];
let () = msg_send![item, setRepresentedObject: number];
}
fn menu_item_action(&self, item: id) {
if let Some(delegate) = self.delegate.upgrade() {
let item_id = unsafe {
let object: id = msg_send![item, representedObject];
msg_send![object, longLongValue]
};
delegate.borrow().on_menu_action(self.handle, item_id);
}
}
fn on_menu_will_open(&self) {
if let Some(delegate) = self.delegate.upgrade() {
delegate.borrow().on_menu_open(self.handle);
}
}
fn create_menu_item(&self, menu_item: &MenuItem, menu_manager: &MenuManager) -> StrongPtr {
unsafe {
if menu_item.separator {
let res = NSMenuItem::separatorItem(nil);
StrongPtr::retain(res)
} else {
let res = NSMenuItem::alloc(nil).initWithTitle_action_keyEquivalent_(
*to_nsstring(""),
Sel::from_ptr(std::ptr::null()),
*to_nsstring(""),
);
let () = msg_send![res, setTag: ITEM_TAG];
self.update_menu_item(res, menu_item, menu_manager);
StrongPtr::new(res)
}
}
}
}
static MENU_ITEM_TARGET_CLASS: Lazy<&'static Class> = Lazy::new(|| unsafe {
let target_superclass = class!(NSObject);
let mut decl = ClassDecl::new("IMMenuItemTarget", target_superclass).unwrap();
decl.add_ivar::<*mut c_void>("imState");
decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel));
decl.add_method(
sel!(onAction:),
on_action as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(menuWillOpen:),
menu_will_open as extern "C" fn(&Object, Sel, id),
);
decl.register()
});
extern "C" fn dealloc(this: &Object, _sel: Sel) {
unsafe {
let state_ptr = {
let state_ptr: *mut c_void = *this.get_ivar("imState");
state_ptr as *const PlatformMenu
};
Weak::from_raw(state_ptr);
let superclass = superclass(this);
let () = msg_send![super(this, superclass), dealloc];
}
}
extern "C" fn on_action(this: &Object, _sel: Sel, sender: id) {
let state = unsafe {
let state_ptr = {
let state_ptr: *mut c_void = *this.get_ivar("imState");
state_ptr as *const PlatformMenu
};
ManuallyDrop::new(Weak::from_raw(state_ptr))
};
let upgraded = state.upgrade();
if let Some(upgraded) = upgraded {
upgraded.menu_item_action(sender);
}
}
extern "C" fn menu_will_open(this: &Object, _: Sel, _menu: id) {
let state = unsafe {
let state_ptr = {
let state_ptr: *mut c_void = *this.get_ivar("imState");
state_ptr as *const PlatformMenu
};
ManuallyDrop::new(Weak::from_raw(state_ptr))
};
let upgraded = state.upgrade();
if let Some(upgraded) = upgraded {
upgraded.on_menu_will_open();
}
}
#[allow(clippy::branches_sharing_code)]
fn remove_mnemonics(title: &str) -> String {
let mut res = String::new();
let mut mnemonic = false;
for c in title.chars() {
if c == '&' {
if !mnemonic {
mnemonic = true;
continue;
} else {
res.write_char('&').unwrap();
mnemonic = false;
continue;
}
}
res.write_char(c).unwrap();
}
res
}