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
This commit is contained in:
Max Stoumen 2023-06-19 12:34:07 -07:00 committed by GitHub
parent 93c32f733f
commit fabbbacb4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 227 additions and 33 deletions

View file

@ -0,0 +1,5 @@
---
"muda": minor
---
Add support for `AboutMetadata` on macOS

View file

@ -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()
}
}

View file

@ -171,7 +171,7 @@ impl MenuChild {
fn set_icon(&mut self, icon: Option<Icon>) {
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::<gtk::Box>().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();

View file

@ -32,4 +32,41 @@ impl PlatformIcon {
png
}
pub unsafe fn to_nsimage(&self, fixed_height: Option<f64>) -> 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
}
}

View file

@ -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<id> = Default::default();
let mut objects: Vec<id> = 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 {

View file

@ -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 {

View file

@ -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<String>,
/// The application version.
pub version: Option<String>,
/// The short version, e.g. "1.0".
///
/// ## Platform-specific
///
/// - **Windows / Linux:** Appended to the end of `version` in parentheses.
pub short_version: Option<String>,
/// The authors of the application.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub authors: Option<Vec<String>>,
/// Application comments.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub comments: Option<String>,
/// The copyright of the application.
pub copyright: Option<String>,
/// The license of the application.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub license: Option<String>,
/// The application website.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub website: Option<String>,
/// The website label.
///
/// ## Platform-specific
///
/// - **macOS:** Unsupported.
pub website_label: Option<String>,
/// The credits.
///
/// ## Platform-specific
///
/// - **Windows / Linux:** Unsupported.
pub credits: Option<String>,
/// The application icon.
///
/// ## Platform-specific
///
/// - **Windows:** Unsupported.
pub icon: Option<Icon>,
}
#[derive(PartialEq, Eq, Debug, Clone)]
impl AboutMetadata {
pub(crate) fn full_version(&self) -> Option<String> {
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,