Expose NSApp on macOS to add app delegates (e.g. "Open With ... " functionality)
Is your feature request related to a problem? Please describe.
I want to create a GUI application that can open CSV files. On macOS, if you put the following lines in the info.plist of the Application.app:
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>csv</string>
</array>
<key>CFBundleTypeName</key>
<string>Comma separated values</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
This then tells Finder that you can "Open With ..." and drag/drop files on the icon of the app (not in the window - this is handled by egui with dropped_files).
However, upon dropping or using "Open With ...", it fails because internally no app-delegate is defined.
Describe the solution you'd like
As far as I understand, an app-delegate somewhat like this:
unsafe {
// Initialize NSApplication
let app = NSApp();
app.setActivationPolicy(cocoa::appkit::NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular);
// Set up the delegate
let delegate = create_app_delegate();
let _: () = msg_send![app, setDelegate: delegate];
// Run the macOS application (This is blocking)
app.run();
}
would need to be defined. But as mentioned in #3411 this cannot be done outside of egui, since egui already creates the NSApp.
Describe alternatives you've considered
I tried putting the code outside of egui in a thread in my main() function and this is the error:
winit requires control over the principal class. You must create the event loop before other parts of your application initialize NSApplication
Additional context
It probably needs to be handled inside egui somewhere around https://github.com/emilk/egui/blob/cf965aaa30987a5b6fa2380f37c0ce8cb869347d/crates/eframe/src/native/app_icon.rs#L202
and exposing the possibility to add app-delegates can also then open up the possibility to add native menu items (#3411)
I am not sure if my approach is going in the correct direction, but I would be fond of the possibility to easily implement "Open With ..." and "Menu Items".
An inspiration: https://github.com/neovide/neovide/blob/38fd9d75419dc00dd2733965f7aa753d759021af/src/window/macos.rs#L512
Okay so by changing the function inside egui to this:
/// Set icon & app title for `MacOS` applications.
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
profiling::function_scope!();
use objc2::{
msg_send,
rc::{autoreleasepool, Retained},
runtime::{AnyClass, AnyObject, ClassBuilder},
sel, ClassType,
};
use objc2_app_kit::{NSApplication, NSImage};
use objc2::runtime::Sel;
use objc2_foundation::{
MainThreadMarker, NSArray, NSData, NSDictionary, NSString, NSUserDefaults,
};
let png_bytes = if let Some(icon_data) = icon_data {
match icon_data.to_png_bytes() {
Ok(png_bytes) => Some(png_bytes),
Err(err) => {
log::warn!("Failed to convert IconData to png: {err}");
return AppIconStatus::NotSetIgnored;
}
}
} else {
None
};
// SAFETY: we don't do anything dangerous here
extern "C" fn handle_open_files(_this: &mut AnyObject, _sel: Sel, _sender: &objc2::runtime::AnyObject, files: &mut NSArray<NSString>) {
autoreleasepool(|pool| {
for file in files.iter() {
let path = file.as_str(pool).to_owned();
println!("Received file via Open With: {}", path);
// Your logic to open and handle the CSV file
}
});
}
let mtm = MainThreadMarker::new().expect("File handler must be registered on main thread.");
unsafe {
let app = NSApplication::sharedApplication(mtm);
if let Some(png_bytes) = png_bytes {
let data = NSData::from_vec(png_bytes);
log::trace!("NSImage::initWithData…");
let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
profiling::scope!("setApplicationIconImage_");
log::trace!("setApplicationIconImage…");
app.setApplicationIconImage(app_icon.as_deref());
}
let delegate = app.delegate().unwrap();
// Find out class of the NSApplicationDelegate
let class: &AnyClass = msg_send![&delegate, class];
// register subclass of whatever was in delegate
let mut my_class = ClassBuilder::new("ApplicationDelegate", class).unwrap();
my_class.add_method(
sel!(application:openFiles:),
handle_open_files as unsafe extern "C" fn(_, _, _, _) -> _,
);
let class = my_class.register();
// this should be safe as:
// * our class is a subclass
// * no new ivars
// * overriden methods are compatible with old (we implement protocol method)
let delegate_obj = Retained::cast::<AnyObject>(delegate);
AnyObject::set_class(&delegate_obj, class);
// Prevent AppKit from interpreting our command line.
let key = NSString::from_str("NSTreatUnknownArgumentsAsOpen");
let keys = vec![key.as_ref()];
let objects = vec![Retained::cast::<AnyObject>(NSString::from_str("YES"))];
let dict = NSDictionary::from_vec(&keys, objects);
NSUserDefaults::standardUserDefaults().registerDefaults(dict.as_ref());
// Change the title in the top bar - for python processes this would be again "python" otherwise.
if let Some(main_menu) = app.mainMenu() {
if let Some(item) = main_menu.itemAtIndex(0) {
if let Some(app_menu) = item.submenu() {
profiling::scope!("setTitle_");
app_menu.setTitle(&NSString::from_str(title));
}
}
}
}
// The title in the Dock apparently can't be changed.
// At least these people didn't figure it out either:
// https://stackoverflow.com/questions/69831167/qt-change-application-title-dynamically-on-macos
// https://stackoverflow.com/questions/28808226/changing-cocoa-app-icon-title-and-menu-labels-at-runtime
AppIconStatus::Set
}
I managed to get the "drop on icon" feature to work. However, it appears that this function gets called too late in egui, such that the delegates are not immediately set up after app launch. Thus, when you call "Open with ..." macOS does not see the delegate and reports that this application cannot open this type of file.
Is there any way to change the call of this method in egui such that it is earlier? I also notice that when launching the app, the icon appears significantly late - which would confirm my theory that this is the culprit.
This will not be necessary once we will upgrade winit. There is a discussion going on here: https://github.com/rust-windowing/winit/issues/4260