mirror of
https://github.com/italicsjenga/muda.git
synced 2025-01-11 04:11:32 +11:00
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:
parent
93c32f733f
commit
fabbbacb4b
5
.changes/macos-about-metadata.md
Normal file
5
.changes/macos-about-metadata.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"muda": minor
|
||||
---
|
||||
|
||||
Add support for `AboutMetadata` on macOS
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue