use incular_platform::{ApplicationActivation, DocumentActivation, UrlActivation};
use objc2::{
ClassType, DeclaredClass, declare_class, msg_send_id, mutability, rc::Retained,
runtime::ProtocolObject,
};
use objc2_app_kit::{NSApplication, NSApplicationDelegate};
use objc2_foundation::{MainThreadMarker, NSArray, NSObject, NSObjectProtocol, NSString, NSURL};
use std::{cell::RefCell, path::PathBuf, rc::Rc, sync::Arc};
use url::Url;
struct ActivationIvars {
deliver: Arc<dyn Fn(ApplicationActivation) + Send + Sync>,
}
declare_class!(
struct IncularApplicationActivationDelegate;
unsafe impl ClassType for IncularApplicationActivationDelegate {
type Super = NSObject;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "IncularApplicationActivationDelegate";
}
impl DeclaredClass for IncularApplicationActivationDelegate {
type Ivars = ActivationIvars;
}
unsafe impl NSObjectProtocol for IncularApplicationActivationDelegate {}
unsafe impl NSApplicationDelegate for IncularApplicationActivationDelegate {
#[method(application:openURLs:)]
fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
for activation in normalize_urls(urls) {
(self.ivars().deliver)(activation);
}
}
#[method(application:openFiles:)]
fn application_open_files(
&self,
_application: &NSApplication,
filenames: &NSArray<NSString>,
) {
let paths = (0..filenames.count())
.map(|index| {
let filename = unsafe { filenames.objectAtIndex(index) };
PathBuf::from(filename.to_string())
})
.collect::<Vec<_>>();
if !paths.is_empty() {
(self.ivars().deliver)(ApplicationActivation::OpenFiles(
DocumentActivation::from_paths(paths),
));
}
}
#[method(applicationShouldHandleReopen:hasVisibleWindows:)]
fn application_should_handle_reopen(
&self,
_application: &NSApplication,
_has_visible_windows: bool,
) -> bool {
(self.ivars().deliver)(ApplicationActivation::Reopen);
true
}
}
);
impl IncularApplicationActivationDelegate {
fn new(
deliver: Arc<dyn Fn(ApplicationActivation) + Send + Sync>,
mtm: MainThreadMarker,
) -> Retained<Self> {
let this = mtm.alloc();
let this = this.set_ivars(ActivationIvars { deliver });
unsafe { msg_send_id![super(this), init] }
}
}
fn normalize_urls(urls: &NSArray<NSURL>) -> Vec<ApplicationActivation> {
enum Batch {
Files(Vec<PathBuf>),
Urls(Vec<Url>),
}
fn flush(batch: Batch, activations: &mut Vec<ApplicationActivation>) {
match batch {
Batch::Files(paths) if !paths.is_empty() => activations.push(
ApplicationActivation::OpenFiles(DocumentActivation::from_paths(paths)),
),
Batch::Urls(urls) if !urls.is_empty() => {
activations.push(ApplicationActivation::OpenUrls(UrlActivation::new(urls)))
}
Batch::Files(_) | Batch::Urls(_) => {}
}
}
let mut activations = Vec::new();
let mut batch: Option<Batch> = None;
for index in 0..urls.count() {
let native = unsafe { urls.objectAtIndex(index) };
let item = unsafe {
if native.isFileURL() {
native
.path()
.map(|path| EitherActivation::File(PathBuf::from(path.to_string())))
} else {
native.absoluteString().and_then(|value| {
Url::parse(&value.to_string())
.ok()
.map(EitherActivation::Url)
})
}
};
let Some(item) = item else {
continue;
};
match (batch.take(), item) {
(Some(Batch::Files(mut paths)), EitherActivation::File(path)) => {
paths.push(path);
batch = Some(Batch::Files(paths));
}
(Some(Batch::Urls(mut parsed)), EitherActivation::Url(url)) => {
parsed.push(url);
batch = Some(Batch::Urls(parsed));
}
(Some(previous), EitherActivation::File(path)) => {
flush(previous, &mut activations);
batch = Some(Batch::Files(vec![path]));
}
(Some(previous), EitherActivation::Url(url)) => {
flush(previous, &mut activations);
batch = Some(Batch::Urls(vec![url]));
}
(None, EitherActivation::File(path)) => batch = Some(Batch::Files(vec![path])),
(None, EitherActivation::Url(url)) => batch = Some(Batch::Urls(vec![url])),
}
}
if let Some(batch) = batch {
flush(batch, &mut activations);
}
activations
}
enum EitherActivation {
File(PathBuf),
Url(Url),
}
#[derive(Clone, Default)]
pub(crate) struct MacosActivationState {
inner: Rc<ActivationInner>,
}
#[derive(Default)]
struct ActivationInner {
delegate: RefCell<Option<Retained<IncularApplicationActivationDelegate>>>,
}
impl MacosActivationState {
pub(crate) fn start_watch(&self, deliver: Arc<dyn Fn(ApplicationActivation) + Send + Sync>) {
if self.inner.delegate.borrow().is_some() {
return;
}
let mtm = MainThreadMarker::new()
.expect("AppKit application activation watch must be installed on the main thread");
let app = NSApplication::sharedApplication(mtm);
let delegate = IncularApplicationActivationDelegate::new(deliver, mtm);
app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
*self.inner.delegate.borrow_mut() = Some(delegate);
}
}
impl Drop for ActivationInner {
fn drop(&mut self) {
if self.delegate.get_mut().is_none() {
return;
}
if let Some(mtm) = MainThreadMarker::new() {
NSApplication::sharedApplication(mtm).setDelegate(None);
}
self.delegate.get_mut().take();
}
}