diff --git a/Cargo.toml b/Cargo.toml index 4af6ecc..73367f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,9 @@ gtk= "0.8.1" glib= "0.9.3" libappindicator= "0.5.1" -# [target.'cfg(target_os = "macos")'.dependencies] -# objc="*" -# cocoa="*" -# core-foundation="*" +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2.7" +objc-foundation = "0.1.1" +objc_id = "0.1.1" +cocoa = "0.20.0" +core-foundation = "0.7.0" diff --git a/README.md b/README.md index 72ef5b6..93741c0 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ from [winapi-rs, by retep998](https://github.com/retep998/winapi-rs). This code is covered under the MIT license. This code will be removed once winapi-rs has a 0.3 crate available. +systray-rs includes some code +from [rust-sysbar, by rust-sysbar](https://github.com/rust-sysbar/rust-sysbar). +This code is covered under the MIT license. + systray-rs is BSD licensed. Copyright (c) 2016-2020, Nonpolynomial Labs, LLC diff --git a/examples/rust-logo.png b/examples/rust-logo.png new file mode 100644 index 0000000..f9668c5 Binary files /dev/null and b/examples/rust-logo.png differ diff --git a/examples/systray-example.rs b/examples/trayicon.rs similarity index 82% rename from examples/systray-example.rs rename to examples/trayicon.rs index 85c1a6e..22a3abf 100644 --- a/examples/systray-example.rs +++ b/examples/trayicon.rs @@ -1,6 +1,5 @@ #![windows_subsystem = "windows"] -//#[cfg(target_os = "windows")] fn main() -> Result<(), systray::Error> { let mut app; match systray::Application::new() { @@ -9,7 +8,11 @@ fn main() -> Result<(), systray::Error> { } // w.set_icon_from_file(&"C:\\Users\\qdot\\code\\git-projects\\systray-rs\\resources\\rust.ico".to_string()); // w.set_tooltip(&"Whatever".to_string()); - app.set_icon_from_file("/usr/share/gxkb/flags/ua.png")?; + + let file_path = std::path::Path::new(file!()); + let icon_path = file_path.with_file_name("rust-logo.png"); + + app.set_icon_from_file(icon_path.to_str().unwrap())?; app.add_menu_item("Print a thing", |_| { println!("Printing a thing!"); @@ -35,9 +38,4 @@ fn main() -> Result<(), systray::Error> { println!("Waiting on message!"); app.wait_for_message()?; Ok(()) -} - -// #[cfg(not(target_os = "windows"))] -// fn main() { -// panic!("Not implemented on this platform!"); -// } +} \ No newline at end of file diff --git a/src/api/cocoa/mod.rs b/src/api/cocoa/mod.rs index bdfcd0d..c454829 100644 --- a/src/api/cocoa/mod.rs +++ b/src/api/cocoa/mod.rs @@ -1,28 +1,305 @@ -use crate::Error; -use std; +//! Contains the implementation of the Mac OS X tray icon in the top bar. -pub struct Window {} +use std::{ + self, + cell::RefCell, + ffi::c_void, + mem, + rc::Rc, + sync::{ + Mutex, + mpsc::Sender, + }, +}; +use cocoa::{ + appkit::{ + NSApp, NSApplication, NSApplicationActivateIgnoringOtherApps, NSButton, NSImage, + NSMenu, NSMenuItem, NSRunningApplication, NSStatusBar, NSStatusItem, + NSSquareStatusItemLength + }, + base::{id, nil, YES}, + foundation::{NSData, NSSize, NSAutoreleasePool, NSString} +}; +use objc::{ + Message, + declare::ClassDecl, + runtime::{Class, Object, Sel} +}; +use objc_foundation::{INSObject, NSObject}; +use objc_id::Id; +use crate::{Application, BoxedError, Callback, Error}; + +/// The general representation of the Mac OS X application. +pub struct Window { + /// A reference to systray::Application for callbacks + systray_application: Option>>>, + /// A mutable reference to the `NSApplication` instance of the currently running application. + application: Mutex, + /// It seems that we have to use `NSAutoreleasePool` to prevent memory leaks. + autorelease_pool: Mutex, + /// `NSMenu` for menu items. + menu: Mutex, +} impl Window { + /// Creates a new instance of the `Window`. pub fn new() -> Result { - Err(Error::NotImplementedError) + Ok(Window { + systray_application: None, + application: unsafe { Mutex::from(NSApp()) }, + autorelease_pool: unsafe { Mutex::from(NSAutoreleasePool::new(nil)) }, + menu: unsafe { Mutex::from(NSMenu::new(nil).autorelease()) }, + }) } - pub fn quit(&self) { - unimplemented!() + + /// Sets the systray application + pub fn set_systray_application(&mut self, application_raw_ptr: *mut Application){ + let application = unsafe { Box::from_raw(application_raw_ptr) }; + self.systray_application = Some(Rc::from(RefCell::from(application))); } + + /// Closes the current application. + pub fn quit(&mut self) { + if let Ok(application) = self.application.get_mut() { + unsafe { application.stop_(nil); } + } + } + + /// Sets the tooltip (not available for this platform). pub fn set_tooltip(&self, _: &str) -> Result<(), Error> { - unimplemented!() + Err(Error::OsError("This operating system does not support tooltips for the tray \ + items".to_owned())) } - pub fn add_menu_item(&self, _: &str, _: F) -> Result - where - F: std::ops::Fn(&Window) -> () + 'static, + + /// Sets the application icon displayed in the tray bar. Accepts a `buffer` to the underlying + /// image, you can pass even encoded PNG images here. Supports the same list of formats as + /// `NSImage`. + pub fn set_icon_from_buffer(&mut self, buffer: &'static [u8], _: u32, _: u32) + -> Result<(), Error> { - unimplemented!() + const ICON_WIDTH: f64 = 18.0; + const ICON_HEIGHT: f64 = 18.0; + + let tray_entry = unsafe { + NSStatusBar::systemStatusBar(nil).statusItemWithLength_(NSSquareStatusItemLength) + .autorelease() + }; + + let nsdata = unsafe { + NSData::dataWithBytes_length_(nil, + buffer.as_ptr() as *const std::os::raw::c_void, + buffer.len() as u64).autorelease() + }; + if nsdata == nil { + return Err(Error::OsError("Could not create `NSData` out of the passed buffer" + .to_owned())); + } + + let nsimage = unsafe { NSImage::initWithData_(NSImage::alloc(nil), nsdata).autorelease() }; + if nsimage == nil { + return Err(Error::OsError("Could not create `NSImage` out of the created \ + `NSData` buffer".to_owned())); + } + + unsafe { + let new_size = NSSize::new(ICON_WIDTH, ICON_HEIGHT); + let _: () = msg_send![nsimage, setSize:new_size]; + tray_entry.button().setImage_(nsimage); + if let Ok(menu) = self.menu.get_mut(){ + tray_entry.setMenu_(*menu); + } + } + + Ok(()) } + + /// Starts the application event loop. Calling this function will block the current thread. pub fn wait_for_message(&mut self) { - unimplemented!() + if let Ok(application) = self.application.get_mut() { + unsafe { + application.activateIgnoringOtherApps_(YES); + NSRunningApplication::currentApplication(nil) + .activateWithOptions_(NSApplicationActivateIgnoringOtherApps); + application.run(); + } + } } - pub fn set_icon_from_buffer(&self, _: &[u8], _: u32, _: u32) -> Result<(), Error> { + + pub fn set_icon_from_resource(&self, resource_name: &str) -> Result<(), Error> { unimplemented!() } + + pub fn set_icon_from_file(&mut self, icon_file: &str) -> Result<(), Error> { + const ICON_WIDTH: f64 = 18.0; + const ICON_HEIGHT: f64 = 18.0; + + let tray_entry = unsafe { + NSStatusBar::systemStatusBar(nil).statusItemWithLength_(NSSquareStatusItemLength) + .autorelease() + }; + + let path = unsafe { + NSString::alloc(nil).init_str(icon_file) + }; + if path == nil { + return Err(Error::OsError("Could not create `NSString` out of the passed &str" + .to_owned())); + } + + let nsimage = unsafe { NSImage::initWithContentsOfFile_(NSImage::alloc(nil), path).autorelease() }; + if nsimage == nil { + return Err(Error::OsError("Could not create `NSImage` out of the created \ + `NSData` buffer".to_owned())); + } + + unsafe { + let new_size = NSSize::new(ICON_WIDTH, ICON_HEIGHT); + let _: () = msg_send![nsimage, setSize:new_size]; + tray_entry.button().setImage_(nsimage); + if let Ok(menu) = self.menu.get_mut(){ + tray_entry.setMenu_(*menu); + } + } + + Ok(()) + } + + pub fn add_menu_separator(&mut self, item_idx: u32) -> Result<(), Error> { + let item = unsafe { + NSMenuItem::separatorItem(nil) + }; + if item == nil { + return Err(Error::OsError("Could not create `NSMenuItem`." + .to_owned())); + } + + unsafe { + if let Ok(menu) = self.menu.get_mut(){ + NSMenu::addItem_(*menu, item); + } + } + + Ok(()) + } + + pub fn add_menu_entry(&mut self, item_idx: u32, item_name: &str, callback: Callback) -> Result<(), Error> { + let blank_key = unsafe { NSString::alloc(nil).init_str("") }; + if blank_key == nil { + return Err(Error::OsError("Could not create blank `NSString`." + .to_owned())); + } + + let title = unsafe { NSString::alloc(nil).init_str(item_name) }; + if title == nil { + return Err(Error::OsError("Could not create `NSString` from the item name." + .to_owned())); + } + + let action = sel!(call); + + let item = unsafe { + NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_(title, action, blank_key) + }; + if item == nil { + return Err(Error::OsError("Could not create `NSMenuItem`." + .to_owned())); + } + + unsafe { + if let Some(app) = &self.systray_application { + let _ : () = msg_send![item, setTarget: CocoaCallback::from(app.clone(), callback)]; + } + if let Ok(menu) = self.menu.get_mut(){ + NSMenu::addItem_(*menu, item); + } + } + + Ok(()) + } + + pub fn shutdown(&self) -> Result<(), Error> { + Ok(()) + } +} + +// Devired from https://github.com/rust-sysbar/rust-sysbar/blob/master/src/mac_os/mod.rs +// Copyright (c) 2017 The rs-barfly Developers +// Copyright (c) 2017 The rust-sysbar Developers + +pub struct CocoaCallbackState { + application: Rc>>, + callback: Callback +} + +enum CocoaCallback {} + +impl CocoaCallback { + pub fn from(application: Rc>>, callback: Callback) -> Id { + let ccs = CocoaCallbackState { + application, + callback + }; + let bccs = Box::new(ccs); + + let ptr = Box::into_raw(bccs); + let ptr = ptr as *mut c_void as usize; + let mut oid = ::new(); + (*oid).setptr(ptr); + oid + } + + fn setptr(&mut self, uptr: usize) { + unsafe { + let obj = &mut *(self as *mut _ as *mut ::objc::runtime::Object); + obj.set_ivar("_cbptr", uptr); + } + } +} + +impl CocoaCallbackState { + pub fn call(&mut self) -> Result<(), BoxedError> { + if let Ok(mut application) = self.application.try_borrow_mut() { + return (*self.callback)(&mut application); + } + Err(Box::from(Error::OsError("Unable to borrow the application".to_owned()))) + } } + +unsafe impl Message for CocoaCallback {} + +impl INSObject for CocoaCallback { + fn class() -> &'static Class { + let cname = "CCCallback"; + + let mut _class = Class::get(cname); + if _class.is_none() { + let superclass = NSObject::class(); + let mut decl = ClassDecl::new(&cname, superclass).unwrap(); + decl.add_ivar::("_cbptr"); + + extern "C" fn sysbar_callback_call(obj: &Object, _: Sel) { + unsafe { + let pointer_value: usize = *obj.get_ivar("_cbptr"); + let callback_pointer = pointer_value as *mut c_void as *mut CocoaCallbackState; + let mut boxed_callback: Box = Box::from_raw(callback_pointer); + { + boxed_callback.call(); + } + mem::forget(boxed_callback); + } + } + + unsafe { + decl.add_method( + sel!(call), + sysbar_callback_call as extern "C" fn(&Object, Sel), + ); + } + + decl.register(); + _class = Class::get(cname); + } + _class.unwrap() + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 36c6dbd..6ac1f6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,16 @@ +// Needed for msg_send! macro +#[cfg(any(target_os = "macos"))] +#[macro_use] +extern crate objc; + // Systray Lib pub mod api; use std::{ + cell::RefCell, collections::HashMap, error, fmt, + rc::Rc, sync::mpsc::{channel, Receiver}, }; @@ -43,10 +50,12 @@ impl fmt::Display for Error { } pub struct Application { - window: api::api::Window, + window: Rc>>, + #[cfg(target_os = "macos")] + window_raw_ptr: *mut api::api::Window, menu_idx: u32, callback: HashMap, - // Each platform-specific window module will set up its own thread for + // Each non-macOS platform-specific window module will set up its own thread for // dealing with the OS main loop. Use this channel for receiving events from // that thread. rx: Receiver, @@ -67,26 +76,55 @@ where } impl Application { - pub fn new() -> Result { + #[cfg(not(target_os = "macos"))] + pub fn new() -> Result, Error> { let (event_tx, event_rx) = channel(); match api::api::Window::new(event_tx) { - Ok(w) => Ok(Application { - window: w, + Ok(w) => Ok(Box::from(Application { + window: Rc::from(RefCell::from(Box::from(w))), menu_idx: 0, callback: HashMap::new(), rx: event_rx, - }), + })), Err(e) => Err(e), } } + #[cfg(target_os = "macos")] + pub fn new() -> Result, Error> { + let (event_tx, event_rx) = channel(); + + match api::api::Window::new() { + Ok(w) => { + let window_raw_ptr = Box::into_raw(Box::from(w)); + let window = unsafe { Box::from_raw(window_raw_ptr) }; + let window_rc = Rc::from(RefCell::from(window)); + let application_raw_ptr = Box::into_raw(Box::from(Application { + window: window_rc.clone(), + window_raw_ptr, + menu_idx: 0, + callback: HashMap::new(), + rx: event_rx, + })); + + let mut application_window = window_rc.borrow_mut(); + application_window.set_systray_application(application_raw_ptr); + + let application = unsafe { Box::from_raw(application_raw_ptr) }; + Ok(application) + }, + Err(e) => Err(e), + } + } + + #[cfg(not(target_os = "macos"))] pub fn add_menu_item(&mut self, item_name: &str, f: F) -> Result where F: FnMut(&mut Application) -> Result<(), E> + Send + Sync + 'static, E: error::Error + Send + Sync + 'static, { let idx = self.menu_idx; - if let Err(e) = self.window.add_menu_entry(idx, item_name) { + if let Err(e) = self.window.try_borrow_mut()?.add_menu_entry(idx, item_name) { return Err(e); } self.callback.insert(idx, make_callback(f)); @@ -94,45 +132,61 @@ impl Application { Ok(idx) } + #[cfg(target_os = "macos")] + pub fn add_menu_item(&mut self, item_name: &str, f: F) -> Result + where + F: FnMut(&mut Application) -> Result<(), E> + Send + Sync + 'static, + E: error::Error + Send + Sync + 'static, + { + let idx = self.menu_idx; + if let Err(e) = self.window.try_borrow_mut()?.add_menu_entry(idx, item_name, make_callback(f)) { + return Err(e); + } + self.menu_idx += 1; + Ok(idx) + } + pub fn add_menu_separator(&mut self) -> Result { let idx = self.menu_idx; - if let Err(e) = self.window.add_menu_separator(idx) { + if let Err(e) = self.window.try_borrow_mut()?.add_menu_separator(idx) { return Err(e); } self.menu_idx += 1; Ok(idx) } - pub fn set_icon_from_file(&self, file: &str) -> Result<(), Error> { - self.window.set_icon_from_file(file) + pub fn set_icon_from_file(&mut self, file: &str) -> Result<(), Error> { + self.window.try_borrow_mut()?.set_icon_from_file(file) } - pub fn set_icon_from_resource(&self, resource: &str) -> Result<(), Error> { - self.window.set_icon_from_resource(resource) + pub fn set_icon_from_resource(&mut self, resource: &str) -> Result<(), Error> { + self.window.try_borrow_mut()?.set_icon_from_resource(resource) } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] pub fn set_icon_from_buffer( - &self, - buffer: &[u8], + &mut self, + buffer: &'static [u8], width: u32, height: u32, ) -> Result<(), Error> { - self.window.set_icon_from_buffer(buffer, width, height) + self.window.try_borrow_mut()?.set_icon_from_buffer(buffer, width, height) } - pub fn shutdown(&self) -> Result<(), Error> { - self.window.shutdown() + pub fn shutdown(&mut self) -> Result<(), Error> { + self.window.try_borrow_mut()?.shutdown() } - pub fn set_tooltip(&self, tooltip: &str) -> Result<(), Error> { - self.window.set_tooltip(tooltip) + pub fn set_tooltip(&mut self, tooltip: &str) -> Result<(), Error> { + self.window.try_borrow_mut()?.set_tooltip(tooltip) } - pub fn quit(&mut self) { - self.window.quit() + pub fn quit(&mut self) -> Result<(), Error> { + self.window.try_borrow_mut()?.quit(); + Ok(()) } + #[cfg(not(target_os = "macos"))] pub fn wait_for_message(&mut self) -> Result<(), Error> { loop { let msg; @@ -153,6 +207,15 @@ impl Application { Ok(()) } + + #[cfg(target_os = "macos")] + pub fn wait_for_message<'a>(&'a mut self) -> Result<(), Error> { + let mut window = unsafe { Box::from_raw(self.window_raw_ptr) }; + window.wait_for_message(); + + Ok(()) + } + } impl Drop for Application { @@ -160,3 +223,9 @@ impl Drop for Application { self.shutdown().ok(); } } + +impl std::convert::From for Error { + fn from(err: std::cell::BorrowMutError) -> Self { + Error::OsError(format!("{}", err)) + } +}