From fabbbacb4b8d77c84cd318a21df1951063e7ea14 Mon Sep 17 00:00:00 2001 From: Max Stoumen Date: Mon, 19 Jun 2023 12:34:07 -0700 Subject: [PATCH] support `AboutMetadata` on macos (#66) * support `AboutMetadata` on macos * cleaner syntax * incorrect docstring * support linux * move "Copyright" to constant * append short_version to version in win, gtk * narrower unsafe scope * more accurate docs * consistent periods in docs * use `logo` instead for gtk * clippy autofix * fmt and clippy * Create macos-about-metadata.md --- .changes/macos-about-metadata.md | 5 ++ src/platform_impl/gtk/icon.rs | 10 ++- src/platform_impl/gtk/mod.rs | 9 ++- src/platform_impl/macos/icon.rs | 37 +++++++++++ src/platform_impl/macos/mod.rs | 106 +++++++++++++++++++++++++------ src/platform_impl/windows/mod.rs | 2 +- src/predefined.rs | 91 ++++++++++++++++++++++++-- 7 files changed, 227 insertions(+), 33 deletions(-) create mode 100644 .changes/macos-about-metadata.md diff --git a/.changes/macos-about-metadata.md b/.changes/macos-about-metadata.md new file mode 100644 index 0000000..4b3dc82 --- /dev/null +++ b/.changes/macos-about-metadata.md @@ -0,0 +1,5 @@ +--- +"muda": minor +--- + +Add support for `AboutMetadata` on macOS diff --git a/src/platform_impl/gtk/icon.rs b/src/platform_impl/gtk/icon.rs index 14e88da..f99071b 100644 --- a/src/platform_impl/gtk/icon.rs +++ b/src/platform_impl/gtk/icon.rs @@ -45,7 +45,7 @@ impl PlatformIcon { }) } - pub fn to_pixbuf(&self, w: i32, h: i32) -> Pixbuf { + pub fn to_pixbuf(&self) -> Pixbuf { Pixbuf::from_mut_slice( self.raw.clone(), gdk_pixbuf::Colorspace::Rgb, @@ -55,7 +55,11 @@ impl PlatformIcon { self.height, self.row_stride, ) - .scale_simple(w, h, gdk_pixbuf::InterpType::Bilinear) - .unwrap() + } + + pub fn to_pixbuf_scale(&self, w: i32, h: i32) -> Pixbuf { + self.to_pixbuf() + .scale_simple(w, h, gdk_pixbuf::InterpType::Bilinear) + .unwrap() } } diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 914cd0d..01af751 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -171,7 +171,7 @@ impl MenuChild { fn set_icon(&mut self, icon: Option) { self.icon = icon.clone(); - let pixbuf = icon.map(|i| i.inner.to_pixbuf(16, 16)); + let pixbuf = icon.map(|i| i.inner.to_pixbuf_scale(16, 16)); for items in self.gtk_menu_items.values() { for i in items { let box_container = i.child().unwrap().downcast::().unwrap(); @@ -965,7 +965,7 @@ impl PredefinedMenuItem { if let Some(name) = &metadata.name { builder = builder.program_name(name); } - if let Some(version) = &metadata.version { + if let Some(version) = &metadata.full_version() { builder = builder.version(version); } if let Some(authors) = &metadata.authors { @@ -986,6 +986,9 @@ impl PredefinedMenuItem { if let Some(website_label) = &metadata.website_label { builder = builder.website_label(website_label); } + if let Some(icon) = &metadata.icon { + builder = builder.logo(&icon.inner.to_pixbuf()); + } let about = builder.build(); about.run(); @@ -1178,7 +1181,7 @@ impl IconMenuItem { let image = self_ .icon .as_ref() - .map(|i| gtk::Image::from_pixbuf(Some(&i.inner.to_pixbuf(16, 16)))) + .map(|i| gtk::Image::from_pixbuf(Some(&i.inner.to_pixbuf_scale(16, 16)))) .unwrap_or_else(gtk::Image::default); self_.accel_group = accel_group.cloned(); diff --git a/src/platform_impl/macos/icon.rs b/src/platform_impl/macos/icon.rs index 3a57374..8b70b8f 100644 --- a/src/platform_impl/macos/icon.rs +++ b/src/platform_impl/macos/icon.rs @@ -32,4 +32,41 @@ impl PlatformIcon { png } + + pub unsafe fn to_nsimage(&self, fixed_height: Option) -> cocoa::base::id { + use cocoa::{ + appkit::NSImage, + base::nil, + foundation::{NSData, NSSize}, + }; + + let (width, height) = self.get_size(); + let icon = self.to_png(); + + let (icon_height, icon_width) = match fixed_height { + Some(fixed_height) => { + let icon_height: f64 = fixed_height; + let icon_width: f64 = (width as f64) / (height as f64 / icon_height); + + (icon_height, icon_width) + } + + None => { + let (icon_height, icon_width) = self.get_size(); + (icon_height as f64, icon_width as f64) + } + }; + + let nsdata = NSData::dataWithBytes_length_( + nil, + icon.as_ptr() as *const std::os::raw::c_void, + icon.len() as u64, + ); + + let nsimage = NSImage::initWithData_(NSImage::alloc(nil), nsdata); + let new_size = NSSize::new(icon_width, icon_height); + let _: () = msg_send![nsimage, setSize: new_size]; + + nsimage + } } diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 5a531bb..fff574c 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -11,9 +11,9 @@ pub(crate) use icon::PlatformIcon; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Once}; use cocoa::{ - appkit::{CGFloat, NSApp, NSApplication, NSEventModifierFlags, NSImage, NSMenu, NSMenuItem}, + appkit::{CGFloat, NSApp, NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem}, base::{id, nil, selector, NO, YES}, - foundation::{NSAutoreleasePool, NSData, NSInteger, NSPoint, NSRect, NSSize, NSString}, + foundation::{NSArray, NSAutoreleasePool, NSDictionary, NSInteger, NSPoint, NSRect, NSString}, }; use objc::{ declare::ClassDecl, @@ -32,6 +32,19 @@ use crate::{ static COUNTER: Counter = Counter::new(); static BLOCK_PTR: &str = "mudaMenuItemBlockPtr"; +#[link(name = "AppKit", kind = "framework")] +extern "C" { + static NSAboutPanelOptionApplicationName: id; + static NSAboutPanelOptionApplicationIcon: id; + static NSAboutPanelOptionApplicationVersion: id; + static NSAboutPanelOptionCredits: id; + static NSAboutPanelOptionVersion: id; +} + +/// https://developer.apple.com/documentation/appkit/nsapplication/1428479-orderfrontstandardaboutpanelwith#discussion +#[allow(non_upper_case_globals)] +const NSAboutPanelOptionCopyright: &str = "Copyright"; + /// A generic child in a menu /// /// Be careful when cloning this item and treat it as read-only @@ -640,11 +653,22 @@ impl PredefinedMenuItem { _ => create_ns_menu_item(&child.text, item_type.selector(), &child.accelerator), }; + if let PredfinedMenuItemType::About(_) = child.predefined_item_type { + unsafe { + let _: () = msg_send![ns_menu_item, setTarget: ns_menu_item]; + let _: () = msg_send![ns_menu_item, setTag:child.id()]; + + // Store a raw pointer to the `MenuChild` as an instance variable on the native menu item + let ptr = Box::into_raw(Box::new(&*child)); + (*ns_menu_item).set_ivar(BLOCK_PTR, ptr as usize); + } + } + unsafe { if !child.enabled { let () = msg_send![ns_menu_item, setEnabled: NO]; } - if child.predefined_item_type == PredfinedMenuItemType::Services { + if let PredfinedMenuItemType::Services = child.predefined_item_type { // we have to assign an empty menu as the app's services menu, and macOS will populate it let services_menu = NSMenu::new(nil).autorelease(); let () = msg_send![NSApp(), setServicesMenu: services_menu]; @@ -858,7 +882,8 @@ impl PredfinedMenuItemType { PredfinedMenuItemType::ShowAll => Some(selector("unhideAllApplications:")), PredfinedMenuItemType::CloseWindow => Some(selector("performClose:")), PredfinedMenuItemType::Quit => Some(selector("terminate:")), - PredfinedMenuItemType::About(_) => Some(selector("orderFrontStandardAboutPanel:")), + // manual implementation in `fire_menu_item_click` + PredfinedMenuItemType::About(_) => Some(selector("fireMenuItemAction:")), PredfinedMenuItemType::Services => None, PredfinedMenuItemType::None => None, } @@ -988,6 +1013,63 @@ extern "C" fn fire_menu_item_click(this: &Object, _: Sel, _item: id) { let ptr: usize = *this.get_ivar(BLOCK_PTR); let item = ptr as *mut &mut MenuChild; + if let PredfinedMenuItemType::About(about_meta) = &(*item).predefined_item_type { + match about_meta { + Some(about_meta) => { + unsafe fn mkstr(s: &str) -> id { + NSString::alloc(nil).init_str(s) + } + + let mut keys: Vec = Default::default(); + let mut objects: Vec = Default::default(); + + if let Some(name) = &about_meta.name { + keys.push(NSAboutPanelOptionApplicationName); + objects.push(mkstr(name)); + } + + if let Some(version) = &about_meta.version { + keys.push(NSAboutPanelOptionApplicationVersion); + objects.push(mkstr(version)); + } + + if let Some(short_version) = &about_meta.short_version { + keys.push(NSAboutPanelOptionVersion); + objects.push(mkstr(short_version)); + } + + if let Some(copyright) = &about_meta.copyright { + keys.push(mkstr(NSAboutPanelOptionCopyright)); + objects.push(mkstr(copyright)); + } + + if let Some(icon) = &about_meta.icon { + keys.push(NSAboutPanelOptionApplicationIcon); + objects.push(icon.inner.to_nsimage(None)); + } + + if let Some(credits) = &about_meta.credits { + keys.push(NSAboutPanelOptionCredits); + let attributed_str: id = msg_send![class!(NSAttributedString), alloc]; + let _: () = msg_send![attributed_str, initWithString: mkstr(credits)]; + objects.push(attributed_str); + } + + let keys_array = NSArray::arrayWithObjects(nil, &keys); + let objs_array = NSArray::arrayWithObjects(nil, &objects); + + let dict = + NSDictionary::dictionaryWithObjects_forKeys_(nil, objs_array, keys_array); + + let _: () = msg_send![NSApp(), orderFrontStandardAboutPanelWithOptions: dict]; + } + + None => { + let _: () = msg_send![NSApp(), orderFrontStandardAboutPanel: this]; + } + } + } + if (*item).type_ == MenuItemType::Check { (*item).set_checked(!(*item).is_checked()); } @@ -1028,22 +1110,8 @@ fn create_ns_menu_item( fn menuitem_set_icon(menuitem: id, icon: Option<&Icon>) { if let Some(icon) = icon { - let (width, height) = icon.inner.get_size(); - let icon = icon.inner.to_png(); - - let icon_height: f64 = 18.0; - let icon_width: f64 = (width as f64) / (height as f64 / icon_height); - unsafe { - let nsdata = NSData::dataWithBytes_length_( - nil, - icon.as_ptr() as *const std::os::raw::c_void, - icon.len() as u64, - ); - - let nsimage = NSImage::initWithData_(NSImage::alloc(nil), nsdata); - let new_size = NSSize::new(icon_width, icon_height); - let _: () = msg_send![nsimage, setSize: new_size]; + let nsimage = icon.inner.to_nsimage(Some(18.)); let _: () = msg_send![menuitem, setImage: nsimage]; } } else { diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 5a1a1e8..6ac0cca 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -1283,7 +1283,7 @@ fn show_about_dialog(hwnd: HWND, metadata: &AboutMetadata) { if let Some(name) = &metadata.name { let _ = writeln!(&mut message, "Name: {}", name); } - if let Some(version) = &metadata.version { + if let Some(version) = &metadata.full_version() { let _ = writeln!(&mut message, "Version: {}", version); } if let Some(authors) = &metadata.authors { diff --git a/src/predefined.rs b/src/predefined.rs index 60e8152..c7c7a92 100644 --- a/src/predefined.rs +++ b/src/predefined.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::{accelerator::Accelerator, MenuItemExt, MenuItemType}; +use crate::{accelerator::Accelerator, icon::Icon, MenuItemExt, MenuItemType}; use keyboard_types::{Code, Modifiers}; #[cfg(target_os = "macos")] @@ -178,32 +178,109 @@ impl PredefinedMenuItem { } /// Application metadata for the [`PredefinedMenuItem::about`]. -/// -/// ## Platform-specific -/// -/// - **macOS:** The metadata is ignored. -#[derive(PartialEq, Eq, Debug, Clone, Default)] +#[derive(Debug, Clone, Default)] pub struct AboutMetadata { /// The application name. pub name: Option, /// The application version. pub version: Option, + /// The short version, e.g. "1.0". + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Appended to the end of `version` in parentheses. + pub short_version: Option, /// The authors of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. pub authors: Option>, /// Application comments. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. pub comments: Option, /// The copyright of the application. pub copyright: Option, /// The license of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. pub license: Option, /// The application website. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. pub website: Option, /// The website label. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. pub website_label: Option, + /// The credits. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Unsupported. + pub credits: Option, + /// The application icon. + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. + pub icon: Option, } -#[derive(PartialEq, Eq, Debug, Clone)] +impl AboutMetadata { + pub(crate) fn full_version(&self) -> Option { + Some(format!( + "{}{}", + (self.version.as_ref())?, + (self.short_version.as_ref()) + .map(|v| format!(" ({v})")) + .unwrap_or_default() + )) + } +} + +#[test] +fn test_about_metadata() { + assert_eq!( + AboutMetadata { + ..Default::default() + } + .full_version(), + None + ); + + assert_eq!( + AboutMetadata { + version: Some("Version: 1.0".into()), + ..Default::default() + } + .full_version(), + Some("Version: 1.0".into()) + ); + + assert_eq!( + AboutMetadata { + version: Some("Version: 1.0".into()), + short_version: Some("Universal".into()), + ..Default::default() + } + .full_version(), + Some("Version: 1.0 (Universal)".into()) + ); +} + +#[derive(Debug, Clone)] #[non_exhaustive] +#[allow(clippy::large_enum_variant)] pub(crate) enum PredfinedMenuItemType { Separator, Copy,