summary history branches tags files
src/osx/mod.rs
mod rustnsobject;

extern crate objc;
extern crate objc_foundation;
extern crate cocoa;

extern crate fruitbasket;
use self::fruitbasket::FruitApp;

pub use ::TStatusBar;
pub use ::NSCallback;

use objc::runtime::Class;

use self::cocoa::base::{nil, YES};
use self::cocoa::appkit::NSStatusBar;
use self::cocoa::foundation::NSString;
use self::cocoa::appkit::{NSMenu,
                          NSMenuItem,
                          NSImage,
                          NSVariableStatusItemLength,
                          NSStatusItem,
                          NSButton};

use self::rustnsobject::{NSObj, NSObjTrait, NSObjCallbackTrait};

use std::sync::mpsc::Sender;
use std::ptr;
use std::ffi::CStr;

pub type Object = objc::runtime::Object;

pub struct OSXStatusBar {
    object: NSObj,
    app: FruitApp,
    status_bar_item: *mut objc::runtime::Object,
    menu_bar: *mut objc::runtime::Object,
}

impl TStatusBar for OSXStatusBar {
    type S = OSXStatusBar;
    fn new(tx: Sender<String>) -> OSXStatusBar {
        let mut bar;
        unsafe {
            let nsapp = FruitApp::new();
            nsapp.set_activation_policy(fruitbasket::ActivationPolicy::Prohibited);
            let status_bar = NSStatusBar::systemStatusBar(nil);
            bar = OSXStatusBar {
                app: nsapp,
                status_bar_item: status_bar.statusItemWithLength_(NSVariableStatusItemLength),
                menu_bar: NSMenu::new(nil),
                object: NSObj::alloc(tx),
            };

            // Default mode for menu bar items: blue highlight when selected
            let _: () = msg_send![bar.status_bar_item, setHighlightMode:YES];

            // Set title.  Only displayed if image fails to load.
            let title = NSString::alloc(nil).init_str("connectr");
            NSButton::setTitle_(bar.status_bar_item, title);
            let _: () = msg_send![title, release];

            // Look for icon in OS X bundle if there is one, otherwise current dir.
            // See docs/icons.md for explanation of icon files.
            // TODO: Use the full list of search paths.
            let icon_name = "connectr_80px_300dpi";
            let img_path = match fruitbasket::FruitApp::bundled_resource_path(icon_name, "png") {
                Some(path) => path,
                None => format!("{}.png", icon_name),
            };

            // Set the status bar image.  Switching on setTemplate switches it to
            // using OS X system-style icons that are masked to all white.  I
            // prefer color, but that should maybe be configurable.
            let img = NSString::alloc(nil).init_str(&img_path);
            let icon = NSImage::alloc(nil).initWithContentsOfFile_(img);
            #[cfg(feature = "mac_white_icon")]
            let _: () = msg_send![icon, setTemplate: YES]; // enable to make icon white
            bar.status_bar_item.button().setImage_(icon);
            let _: () = msg_send![img, release];
            let _: () = msg_send![icon, release];

            // Add the same image again as an alternate image.  I'm not sure how the
            // blending is performed, but it behaves differently and better if an
            // alt image is specified.  Without an alt image, the icon darkens too
            // much in 'dark mode' when selected, and is too light in 'light mode'.
            let img = NSString::alloc(nil).init_str(&img_path);
            let icon = NSImage::alloc(nil).initWithContentsOfFile_(img);
            let _: () = msg_send![bar.status_bar_item.button(), setAlternateImage: icon];
            let _: () = msg_send![img, release];
            let _: () = msg_send![icon, release];

            bar.status_bar_item.setMenu_(bar.menu_bar);
            bar.object.cb_fn = Some(Box::new(
                move |s, sender| {
                    let cb = s.get_value(sender);
                    cb(sender, &s.tx);
                }
            ));
        }
        bar
    }
    fn can_redraw(&mut self) -> bool {
        true
    }
    fn clear_items(&mut self) {
        unsafe {
            let old_menu = self.menu_bar;
            self.menu_bar = NSMenu::new(nil);
            self.status_bar_item.setMenu_(self.menu_bar);
            let _: () = msg_send![old_menu, removeAllItems];
            let _: () = msg_send![old_menu, release];
        }
    }
    fn set_tooltip(&mut self, text: &str) {
        unsafe {
            let img = NSString::alloc(nil).init_str(text);
            let _: () = msg_send![self.status_bar_item.button(), setToolTip: img];
            let _: () = msg_send![img, release];
        }
    }
    fn add_label(&mut self, label: &str) {
        unsafe {
            let txt = NSString::alloc(nil).init_str(label);
            let quit_key = NSString::alloc(nil).init_str("");
            let app_menu_item = NSMenuItem::alloc(nil)
                .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key);
            let _: () = msg_send![txt, release];
            let _: () = msg_send![quit_key, release];
            self.menu_bar.addItem_(app_menu_item);
            let _: () = msg_send![app_menu_item, release];
        }
    }
    fn add_quit(&mut self, label: &str) {
        unsafe {
            let txt = NSString::alloc(nil).init_str(label);
            let quit_key = NSString::alloc(nil).init_str("");
            let app_menu_item = NSMenuItem::alloc(nil)
                .initWithTitle_action_keyEquivalent_(txt, sel!(terminate:), quit_key);
            let _: () = msg_send![txt, release];
            let _: () = msg_send![quit_key, release];
            self.menu_bar.addItem_(app_menu_item);
            let _: () = msg_send![app_menu_item, release];
        }
    }
    fn add_separator(&mut self) {
        unsafe {
            let cls = Class::get("NSMenuItem").unwrap();
            let sep: *mut Object = msg_send![cls, separatorItem];
            self.menu_bar.addItem_(sep);
        }
    }
    // TODO: whole API should accept menu option.  this whole thing should
    // be split out into its own recursive menu-builder trait.  this is
    // horrible.
    fn add_item(&mut self, menu: Option<*mut Object>,item: &str, callback: NSCallback, selected: bool) -> *mut Object {
        unsafe {
            let txt = NSString::alloc(nil).init_str(item);
            let quit_key = NSString::alloc(nil).init_str("");
            let app_menu_item = NSMenuItem::alloc(nil)
                .initWithTitle_action_keyEquivalent_(txt, self.object.selector(), quit_key);
            let _: () = msg_send![txt, release];
            let _: () = msg_send![quit_key, release];
            self.object.add_callback(app_menu_item, callback);
            let objc = self.object.take_objc();
            let _: () = msg_send![app_menu_item, setTarget: objc];
            if selected {
                let _: () = msg_send![app_menu_item, setState: 1];
            }
            let item: *mut Object = app_menu_item;
            match menu {
                Some(menu) => { menu.addItem_(app_menu_item); },
                None => { self.menu_bar.addItem_(app_menu_item); }
            }
            let _: () = msg_send![app_menu_item, release];
            item
        }
    }
    fn add_submenu(&mut self, label: &str, callback: NSCallback) -> *mut Object {
        unsafe {
            let submenu = NSMenu::new(nil);
            let txt = NSString::alloc(nil).init_str(label);
            let quit_key = NSString::alloc(nil).init_str("");
            let app_menu_item = NSMenuItem::alloc(nil)
                .initWithTitle_action_keyEquivalent_(txt,
                                                     self.object.selector(),
                                                     quit_key);
            self.object.add_callback(app_menu_item, callback);
            let objc = self.object.take_objc();
            let _: () = msg_send![app_menu_item, setTarget: objc];
            let _: () = msg_send![app_menu_item, setSubmenu: submenu];
            let _: () = msg_send![txt, release];
            let _: () = msg_send![quit_key, release];
            self.menu_bar.addItem_(app_menu_item);
            let _: () = msg_send![app_menu_item, release];
            let _: () = msg_send![submenu, release];
            submenu
        }
    }
    fn update_item(&mut self, item: *mut Object, label: &str) {
        unsafe {
            let ns_label = NSString::alloc(nil).init_str(label);
            let _: () = msg_send![item, setTitle: ns_label];
            let _: () = msg_send![ns_label, release];
        }
    }
    fn sel_item(&mut self, sender: u64) {
        let target: *mut Object = sender as *mut Object;
        unsafe {
            let _: () = msg_send![target, setState: 1];
        }
    }
    fn unsel_item(&mut self, sender: u64) {
        let target: *mut Object = sender as *mut Object;
        unsafe {
            let _: () = msg_send![target, setState: 0];
        }
    }
    fn register_url_handler(&mut self) {
        unsafe {
            let cls = Class::get("NSAppleEventManager").unwrap();
            let manager: *mut Object = msg_send![cls, sharedAppleEventManager];
            let objc = self.object.take_objc();
            let _ = msg_send![objc, handleURLEvent: 0 withReplyEvent: 0];
            let _ = msg_send![manager, setEventHandler: objc
                              andSelector: sel!(handleURLEvent:withReplyEvent:)
                              forEventClass: 0x4755524c
                              andEventID: 0x4755524c];
            info!("Registered URL handler");
            //let cb: NSCallback = Box::new(move |_sender, _tx| {
            //    info!("URL callback");
            //});
            //self.object.add_callback(objc, cb);
        }
    }
    fn run(&mut self, block: bool) {
        let period = match block {
            true => fruitbasket::RunPeriod::Forever,
            _ => fruitbasket::RunPeriod::Once,
        };
        let _ = self.app.run(period);
    }
}

//pub fn osx_alert(text: &str) {
//    unsafe {
//        let ns_text = NSString::alloc(nil).init_str(text);
//        let button = NSString::alloc(nil).init_str("ok");
//        let cls = Class::get("NSAlert").unwrap();
//        let alert: *mut Object = msg_send![cls, alloc];
//        let _: () = msg_send![alert, init];
//        let _: () = msg_send![alert, setMessageText: ns_text];
//        let _: () = msg_send![alert, addButtonWithTitle: button];
//        let _: () = msg_send![alert, runModal];
//        let _: () = msg_send![ns_text, release];
//        let _: () = msg_send![button, release];
//        let _: () = msg_send![alert, release];
//    }
//}

pub fn resource_dir() -> Option<String> {
    unsafe {
        let cls = Class::get("NSBundle").unwrap();
        let bundle: *mut Object = msg_send![cls, mainBundle];
        let path: *mut Object = msg_send![bundle, resourcePath];
        let cstr: *const i8 = msg_send![path, UTF8String];
        if cstr != ptr::null() {
            let rstr = CStr::from_ptr(cstr).to_string_lossy().into_owned();
            return Some(rstr);
        }
        None
    }
}