use std::fmt;
use std::sync::Once;
use block::ConcreteBlock;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl};
use objc_id::Id;
use crate::events::EventModifierFlag;
use crate::foundation::{id, nil, NSString, NSUInteger};
static BLOCK_PTR: &'static str = "cacaoMenuItemBlockPtr";
pub struct Action(Box<dyn Fn() + 'static>);
impl fmt::Debug for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let ptr = format!("{:p}", self.0);
f.debug_struct("Action").field("fn", &ptr).finish()
}
}
fn make_menu_item<S: AsRef<str>>(
title: S,
key: Option<&str>,
action: Option<Sel>,
modifiers: Option<&[EventModifierFlag]>
) -> Id<Object> {
unsafe {
let title = NSString::new(title.as_ref());
let key = NSString::new(match key {
Some(s) => s,
None => ""
});
let alloc: id = msg_send![register_menu_item_class(), alloc];
let item = Id::from_retained_ptr(match action {
Some(a) => msg_send![alloc, initWithTitle:&*title action:a keyEquivalent:&*key],
None => msg_send![alloc, initWithTitle:&*title
action:sel!(fireBlockAction:)
keyEquivalent:&*key]
});
if let Some(modifiers) = modifiers {
let mut key_mask: NSUInteger = 0;
for modifier in modifiers {
let y: NSUInteger = modifier.into();
key_mask = key_mask | y;
}
let _: () = msg_send![&*item, setKeyEquivalentModifierMask: key_mask];
}
item
}
}
#[derive(Debug)]
pub enum MenuItem {
Custom(Id<Object>),
About(String),
Hide,
Services,
HideOthers,
ShowAll,
CloseWindow,
Quit,
Copy,
Cut,
Undo,
Redo,
SelectAll,
Paste,
EnterFullScreen,
Minimize,
Zoom,
ToggleSidebar,
Separator
}
impl MenuItem {
pub(crate) unsafe fn to_objc(self) -> Id<Object> {
match self {
Self::Custom(objc) => objc,
Self::About(app_name) => {
let title = format!("About {}", app_name);
make_menu_item(&title, None, Some(sel!(orderFrontStandardAboutPanel:)), None)
},
Self::Hide => make_menu_item("Hide", Some("h"), Some(sel!(hide:)), None),
Self::Services => {
let item = make_menu_item("Services", None, None, None);
let app: id = msg_send![class!(RSTApplication), sharedApplication];
let services: id = msg_send![app, servicesMenu];
let _: () = msg_send![&*item, setSubmenu: services];
item
},
Self::HideOthers => make_menu_item(
"Hide Others",
Some("h"),
Some(sel!(hide:)),
Some(&[EventModifierFlag::Command, EventModifierFlag::Option])
),
Self::ShowAll => make_menu_item("Show All", None, Some(sel!(unhideAllApplications:)), None),
Self::CloseWindow => make_menu_item("Close Window", Some("w"), Some(sel!(performClose:)), None),
Self::Quit => make_menu_item("Quit", Some("q"), Some(sel!(terminate:)), None),
Self::Copy => make_menu_item("Copy", Some("c"), Some(sel!(copy:)), None),
Self::Cut => make_menu_item("Cut", Some("x"), Some(sel!(cut:)), None),
Self::Undo => make_menu_item("Undo", Some("z"), Some(sel!(undo:)), None),
Self::Redo => make_menu_item("Redo", Some("Z"), Some(sel!(redo:)), None),
Self::SelectAll => make_menu_item("Select All", Some("a"), Some(sel!(selectAll:)), None),
Self::Paste => make_menu_item("Paste", Some("v"), Some(sel!(paste:)), None),
Self::EnterFullScreen => make_menu_item(
"Enter Full Screen",
Some("f"),
Some(sel!(toggleFullScreen:)),
Some(&[EventModifierFlag::Command, EventModifierFlag::Control])
),
Self::Minimize => make_menu_item("Minimize", Some("m"), Some(sel!(performMiniaturize:)), None),
Self::Zoom => make_menu_item("Zoom", None, Some(sel!(performZoom:)), None),
Self::ToggleSidebar => make_menu_item(
"Toggle Sidebar",
Some("s"),
Some(sel!(toggleSidebar:)),
Some(&[EventModifierFlag::Command, EventModifierFlag::Option])
),
Self::Separator => {
let cls = class!(NSMenuItem);
let separator: id = msg_send![cls, separatorItem];
Id::from_ptr(separator)
}
}
}
pub fn new<S: AsRef<str>>(title: S) -> Self {
MenuItem::Custom(make_menu_item(title, None, None, None))
}
pub fn key(self, key: &str) -> Self {
if let MenuItem::Custom(objc) = self {
unsafe {
let key = NSString::new(key);
let _: () = msg_send![&*objc, setKeyEquivalent: key];
}
return MenuItem::Custom(objc);
}
self
}
pub fn modifiers(self, modifiers: &[EventModifierFlag]) -> Self {
if let MenuItem::Custom(objc) = self {
let mut key_mask: NSUInteger = 0;
for modifier in modifiers {
let y: NSUInteger = modifier.into();
key_mask = key_mask | y;
}
unsafe {
let _: () = msg_send![&*objc, setKeyEquivalentModifierMask: key_mask];
}
return MenuItem::Custom(objc);
}
self
}
pub fn action<F: Fn() + 'static>(self, action: F) -> Self {
if let MenuItem::Custom(mut objc) = self {
let handler = Box::new(Action(Box::new(action)));
let ptr = Box::into_raw(handler);
unsafe {
(&mut *objc).set_ivar(BLOCK_PTR, ptr as usize);
let _: () = msg_send![&*objc, setTarget:&*objc];
}
return MenuItem::Custom(objc);
}
self
}
}
extern "C" fn dealloc_cacao_menuitem(this: &Object, _: Sel) {
unsafe {
let ptr: usize = *this.get_ivar(BLOCK_PTR);
let obj = ptr as *mut Action;
if !obj.is_null() {
let _handler = Box::from_raw(obj);
}
let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc];
}
}
extern "C" fn fire_block_action(this: &Object, _: Sel, _item: id) {
let action = crate::utils::load::<Action>(this, BLOCK_PTR);
(action.0)();
}
pub(crate) fn register_menu_item_class() -> *const Class {
static mut APP_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSMenuItem);
let mut decl = ClassDecl::new("CacaoMenuItem", superclass).unwrap();
decl.add_ivar::<usize>(BLOCK_PTR);
decl.add_method(sel!(dealloc), dealloc_cacao_menuitem as extern "C" fn(&Object, _));
decl.add_method(sel!(fireBlockAction:), fire_block_action as extern "C" fn(&Object, _, id));
APP_CLASS = decl.register();
});
unsafe { APP_CLASS }
}