Desktop: Register the Graphite app as a file handler on Mac (#4106)
* Desktop: Implement native file open handler * Desktop: Register file types on Mac * Review
This commit is contained in:
parent
8ae8c47fe1
commit
1c2ac19b16
|
|
@ -13,6 +13,9 @@ const EXEC_PATH: &str = "Contents/MacOS";
|
||||||
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
|
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
|
||||||
const RESOURCES_PATH: &str = "Contents/Resources";
|
const RESOURCES_PATH: &str = "Contents/Resources";
|
||||||
const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework";
|
const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework";
|
||||||
|
const GRAPHITE_DOCUMENT_TYPE: &str = "art.graphite.document";
|
||||||
|
const GRAPHITE_FILE_EXTENSION: &str = "graphite";
|
||||||
|
const GRAPHITE_MIME_TYPE: &str = "application/graphite+json";
|
||||||
|
|
||||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let app_bin = build_bin("graphite-desktop-platform-mac", None)?;
|
let app_bin = build_bin("graphite-desktop-platform-mac", None)?;
|
||||||
|
|
@ -73,7 +76,7 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) ->
|
||||||
cf_bundle_identifier: id.to_string(),
|
cf_bundle_identifier: id.to_string(),
|
||||||
cf_bundle_display_name: exec_name.to_string(),
|
cf_bundle_display_name: exec_name.to_string(),
|
||||||
cf_bundle_executable: exec_name.to_string(),
|
cf_bundle_executable: exec_name.to_string(),
|
||||||
cf_bundle_icon_file: ICONS_FILE_NAME.to_string(),
|
cf_bundle_icon_file: if is_helper { None } else { Some(ICONS_FILE_NAME.to_string()) },
|
||||||
cf_bundle_info_dictionary_version: "6.0".to_string(),
|
cf_bundle_info_dictionary_version: "6.0".to_string(),
|
||||||
cf_bundle_package_type: "APPL".to_string(),
|
cf_bundle_package_type: "APPL".to_string(),
|
||||||
cf_bundle_signature: "????".to_string(),
|
cf_bundle_signature: "????".to_string(),
|
||||||
|
|
@ -85,6 +88,8 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) ->
|
||||||
ls_minimum_system_version: "11.0".to_string(),
|
ls_minimum_system_version: "11.0".to_string(),
|
||||||
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
|
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
|
||||||
ns_supports_automatic_graphics_switching: true,
|
ns_supports_automatic_graphics_switching: true,
|
||||||
|
cf_bundle_document_types: (!is_helper).then(document_types),
|
||||||
|
ut_exported_type_declarations: (!is_helper).then(exported_type_declarations),
|
||||||
};
|
};
|
||||||
|
|
||||||
let plist_file = dir.join("Info.plist");
|
let plist_file = dir.join("Info.plist");
|
||||||
|
|
@ -92,6 +97,47 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) ->
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn document_types() -> Vec<DocumentType> {
|
||||||
|
vec![
|
||||||
|
DocumentType {
|
||||||
|
cf_bundle_type_name: "Graphite Document".to_string(),
|
||||||
|
cf_bundle_type_role: "Editor".to_string(),
|
||||||
|
cf_bundle_type_extensions: Some(vec![GRAPHITE_FILE_EXTENSION.to_string()]),
|
||||||
|
cf_bundle_type_icon_file: Some(ICONS_FILE_NAME.to_string()),
|
||||||
|
ls_handler_rank: Some("Owner".to_string()),
|
||||||
|
ls_item_content_types: vec![GRAPHITE_DOCUMENT_TYPE.to_string()],
|
||||||
|
},
|
||||||
|
DocumentType {
|
||||||
|
cf_bundle_type_name: "SVG Image".to_string(),
|
||||||
|
cf_bundle_type_role: "Editor".to_string(),
|
||||||
|
cf_bundle_type_extensions: Some(vec!["svg".to_string()]),
|
||||||
|
cf_bundle_type_icon_file: None,
|
||||||
|
ls_handler_rank: Some("Alternate".to_string()),
|
||||||
|
ls_item_content_types: vec!["public.svg-image".to_string()],
|
||||||
|
},
|
||||||
|
DocumentType {
|
||||||
|
cf_bundle_type_name: "Image".to_string(),
|
||||||
|
cf_bundle_type_role: "Editor".to_string(),
|
||||||
|
cf_bundle_type_extensions: None,
|
||||||
|
cf_bundle_type_icon_file: None,
|
||||||
|
ls_handler_rank: Some("Alternate".to_string()),
|
||||||
|
ls_item_content_types: vec!["public.image".to_string()],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exported_type_declarations() -> Vec<ExportedTypeDeclaration> {
|
||||||
|
vec![ExportedTypeDeclaration {
|
||||||
|
ut_type_identifier: GRAPHITE_DOCUMENT_TYPE.to_string(),
|
||||||
|
ut_type_description: "Graphite Document".to_string(),
|
||||||
|
ut_type_conforms_to: vec!["public.json".to_string()],
|
||||||
|
ut_type_tag_specification: TypeTagSpecification {
|
||||||
|
public_filename_extension: vec![GRAPHITE_FILE_EXTENSION.to_string()],
|
||||||
|
public_mime_type: GRAPHITE_MIME_TYPE.to_string(),
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct InfoPlist {
|
struct InfoPlist {
|
||||||
#[serde(rename = "CFBundleName")]
|
#[serde(rename = "CFBundleName")]
|
||||||
|
|
@ -103,7 +149,8 @@ struct InfoPlist {
|
||||||
#[serde(rename = "CFBundleExecutable")]
|
#[serde(rename = "CFBundleExecutable")]
|
||||||
cf_bundle_executable: String,
|
cf_bundle_executable: String,
|
||||||
#[serde(rename = "CFBundleIconFile")]
|
#[serde(rename = "CFBundleIconFile")]
|
||||||
cf_bundle_icon_file: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
cf_bundle_icon_file: Option<String>,
|
||||||
#[serde(rename = "CFBundleInfoDictionaryVersion")]
|
#[serde(rename = "CFBundleInfoDictionaryVersion")]
|
||||||
cf_bundle_info_dictionary_version: String,
|
cf_bundle_info_dictionary_version: String,
|
||||||
#[serde(rename = "CFBundlePackageType")]
|
#[serde(rename = "CFBundlePackageType")]
|
||||||
|
|
@ -123,7 +170,53 @@ struct InfoPlist {
|
||||||
#[serde(rename = "LSMinimumSystemVersion")]
|
#[serde(rename = "LSMinimumSystemVersion")]
|
||||||
ls_minimum_system_version: String,
|
ls_minimum_system_version: String,
|
||||||
#[serde(rename = "LSUIElement")]
|
#[serde(rename = "LSUIElement")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
ls_ui_element: Option<String>,
|
ls_ui_element: Option<String>,
|
||||||
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
|
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
|
||||||
ns_supports_automatic_graphics_switching: bool,
|
ns_supports_automatic_graphics_switching: bool,
|
||||||
|
#[serde(rename = "CFBundleDocumentTypes")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
cf_bundle_document_types: Option<Vec<DocumentType>>,
|
||||||
|
#[serde(rename = "UTExportedTypeDeclarations")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
ut_exported_type_declarations: Option<Vec<ExportedTypeDeclaration>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct DocumentType {
|
||||||
|
#[serde(rename = "CFBundleTypeName")]
|
||||||
|
cf_bundle_type_name: String,
|
||||||
|
#[serde(rename = "CFBundleTypeRole")]
|
||||||
|
cf_bundle_type_role: String,
|
||||||
|
#[serde(rename = "CFBundleTypeExtensions")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
cf_bundle_type_extensions: Option<Vec<String>>,
|
||||||
|
#[serde(rename = "CFBundleTypeIconFile")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
cf_bundle_type_icon_file: Option<String>,
|
||||||
|
#[serde(rename = "LSHandlerRank")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
ls_handler_rank: Option<String>,
|
||||||
|
#[serde(rename = "LSItemContentTypes")]
|
||||||
|
ls_item_content_types: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct ExportedTypeDeclaration {
|
||||||
|
#[serde(rename = "UTTypeIdentifier")]
|
||||||
|
ut_type_identifier: String,
|
||||||
|
#[serde(rename = "UTTypeDescription")]
|
||||||
|
ut_type_description: String,
|
||||||
|
#[serde(rename = "UTTypeConformsTo")]
|
||||||
|
ut_type_conforms_to: Vec<String>,
|
||||||
|
#[serde(rename = "UTTypeTagSpecification")]
|
||||||
|
ut_type_tag_specification: TypeTagSpecification,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct TypeTagSpecification {
|
||||||
|
#[serde(rename = "public.filename-extension")]
|
||||||
|
public_filename_extension: Vec<String>,
|
||||||
|
#[serde(rename = "public.mime-type")]
|
||||||
|
public_mime_type: String,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use rand::Rng;
|
||||||
use rfd::AsyncFileDialog;
|
use rfd::AsyncFileDialog;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::mpsc::{Receiver, Sender, SyncSender};
|
use std::sync::mpsc::{Receiver, Sender, SyncSender};
|
||||||
|
|
@ -14,7 +15,6 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||||
use winit::window::WindowId;
|
use winit::window::WindowId;
|
||||||
|
|
||||||
use crate::cef;
|
use crate::cef;
|
||||||
use crate::cli::Cli;
|
|
||||||
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
|
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
|
||||||
use crate::event::{AppEvent, AppEventScheduler};
|
use crate::event::{AppEvent, AppEventScheduler};
|
||||||
use crate::persist;
|
use crate::persist;
|
||||||
|
|
@ -47,7 +47,7 @@ pub(crate) struct App {
|
||||||
web_communication_startup_buffer: Vec<Vec<u8>>,
|
web_communication_startup_buffer: Vec<Vec<u8>>,
|
||||||
#[cfg_attr(not(target_os = "macos"), expect(unused))]
|
#[cfg_attr(not(target_os = "macos"), expect(unused))]
|
||||||
preferences: Preferences,
|
preferences: Preferences,
|
||||||
cli: Cli,
|
launch_documents: Option<Vec<PathBuf>>,
|
||||||
startup_time: Option<Instant>,
|
startup_time: Option<Instant>,
|
||||||
exiting: Arc<AtomicBool>,
|
exiting: Arc<AtomicBool>,
|
||||||
exit_reason: ExitReason,
|
exit_reason: ExitReason,
|
||||||
|
|
@ -58,6 +58,7 @@ impl App {
|
||||||
Window::init();
|
Window::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
cef_context: Box<dyn cef::CefContext>,
|
cef_context: Box<dyn cef::CefContext>,
|
||||||
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
|
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
|
||||||
|
|
@ -65,7 +66,7 @@ impl App {
|
||||||
app_event_receiver: Receiver<AppEvent>,
|
app_event_receiver: Receiver<AppEvent>,
|
||||||
app_event_scheduler: AppEventScheduler,
|
app_event_scheduler: AppEventScheduler,
|
||||||
preferences: Preferences,
|
preferences: Preferences,
|
||||||
cli: Cli,
|
launch_documents: Vec<PathBuf>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
|
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
|
|
@ -115,7 +116,7 @@ impl App {
|
||||||
web_communication_initialized: false,
|
web_communication_initialized: false,
|
||||||
web_communication_startup_buffer: Vec::new(),
|
web_communication_startup_buffer: Vec::new(),
|
||||||
preferences,
|
preferences,
|
||||||
cli,
|
launch_documents: Some(launch_documents),
|
||||||
startup_time: None,
|
startup_time: None,
|
||||||
exiting,
|
exiting,
|
||||||
exit_reason: ExitReason::Shutdown,
|
exit_reason: ExitReason::Shutdown,
|
||||||
|
|
@ -307,22 +308,11 @@ impl App {
|
||||||
responses.push(message);
|
responses.push(message);
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::OpenLaunchDocuments => {
|
DesktopFrontendMessage::OpenLaunchDocuments => {
|
||||||
if self.cli.files.is_empty() {
|
let Some(launch_documents) = std::mem::take(&mut self.launch_documents) else {
|
||||||
|
tracing::error!("OpenLaunchDocuments should only be sent once");
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
let app_event_scheduler = self.app_event_scheduler.clone();
|
self.open_files(launch_documents);
|
||||||
let launch_documents = std::mem::take(&mut self.cli.files);
|
|
||||||
let _ = thread::spawn(move || {
|
|
||||||
for path in launch_documents {
|
|
||||||
tracing::info!("Opening file from command line: {}", path.display());
|
|
||||||
if let Ok(content) = fs::read(&path) {
|
|
||||||
let message = DesktopWrapperMessage::OpenFile { path, content };
|
|
||||||
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
|
||||||
} else {
|
|
||||||
tracing::error!("Failed to read file: {}", path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::UpdateMenu { entries } => {
|
DesktopFrontendMessage::UpdateMenu { entries } => {
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
|
|
@ -476,11 +466,37 @@ impl App {
|
||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
AppEvent::AddLaunchDocuments(paths) => {
|
||||||
|
if let Some(launch_documents) = &mut self.launch_documents {
|
||||||
|
launch_documents.extend(paths);
|
||||||
|
} else {
|
||||||
|
self.open_files(paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
AppEvent::MenuEvent { id } => {
|
AppEvent::MenuEvent { id } => {
|
||||||
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
|
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_files(&mut self, paths: Vec<PathBuf>) {
|
||||||
|
if paths.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let app_event_scheduler = self.app_event_scheduler.clone();
|
||||||
|
let _ = thread::spawn(move || {
|
||||||
|
for path in paths {
|
||||||
|
tracing::info!("Opening file: {}", path.display());
|
||||||
|
if let Ok(content) = fs::read(&path) {
|
||||||
|
let message = DesktopWrapperMessage::OpenFile { path, content };
|
||||||
|
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||||
|
} else {
|
||||||
|
tracing::error!("Failed to read file: {}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl ApplicationHandler for App {
|
impl ApplicationHandler for App {
|
||||||
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
|
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||||
|
|
@ -570,7 +586,7 @@ impl ApplicationHandler for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.cef_init_successful
|
if !self.cef_init_successful
|
||||||
&& !self.cli.disable_ui_acceleration
|
&& !self.preferences.disable_ui_acceleration
|
||||||
&& self.web_communication_initialized
|
&& self.web_communication_initialized
|
||||||
&& let Some(startup_time) = self.startup_time
|
&& let Some(startup_time) = self.startup_time
|
||||||
&& startup_time.elapsed() > Duration::from_secs(3)
|
&& startup_time.elapsed() > Duration::from_secs(3)
|
||||||
|
|
|
||||||
|
|
@ -131,13 +131,13 @@ fn platform_settings(instance_dir: &Path) -> Settings {
|
||||||
{
|
{
|
||||||
let exe = std::env::current_exe().expect("cannot get current exe path");
|
let exe = std::env::current_exe().expect("cannot get current exe path");
|
||||||
let app_root = exe.parent().and_then(|p| p.parent()).expect("bad path structure").parent().expect("bad path structure");
|
let app_root = exe.parent().and_then(|p| p.parent()).expect("bad path structure").parent().expect("bad path structure");
|
||||||
return Settings {
|
Settings {
|
||||||
main_bundle_path: app_root.to_str().map(CefString::from).unwrap(),
|
main_bundle_path: app_root.to_str().map(CefString::from).unwrap(),
|
||||||
multi_threaded_message_loop: 0,
|
multi_threaded_message_loop: 0,
|
||||||
external_message_pump: 1,
|
external_message_pump: 1,
|
||||||
no_sandbox: 1, // GPU helper crashes when running with sandbox
|
no_sandbox: 1, // GPU helper crashes when running with sandbox
|
||||||
..base
|
..base
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ pub(crate) enum AppEvent {
|
||||||
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
||||||
Exit,
|
Exit,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
AddLaunchDocuments(Vec<std::path::PathBuf>),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
MenuEvent {
|
MenuEvent {
|
||||||
id: String,
|
id: String,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ pub fn start() {
|
||||||
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
|
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
|
||||||
dirs::delete_old_cef_browser_directory();
|
dirs::delete_old_cef_browser_directory();
|
||||||
|
|
||||||
let prefs = preferences::read();
|
let mut prefs = preferences::read();
|
||||||
|
|
||||||
// Must be called before event loop initialization or native window integrations will break
|
// Must be called before event loop initialization or native window integrations will break
|
||||||
App::init();
|
App::init();
|
||||||
|
|
@ -80,13 +80,15 @@ pub fn start() {
|
||||||
|
|
||||||
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();
|
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
let disable_ui_acceleration = prefs.disable_ui_acceleration || cli.disable_ui_acceleration;
|
if cli.disable_ui_acceleration {
|
||||||
if disable_ui_acceleration {
|
prefs.disable_ui_acceleration = true;
|
||||||
|
}
|
||||||
|
if prefs.disable_ui_acceleration {
|
||||||
println!("UI acceleration is disabled");
|
println!("UI acceleration is disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver);
|
let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver);
|
||||||
let cef_context = match cef_context_builder.create(cef_handler, disable_ui_acceleration) {
|
let cef_context = match cef_context_builder.create(cef_handler, prefs.disable_ui_acceleration) {
|
||||||
Ok(context) => {
|
Ok(context) => {
|
||||||
tracing::info!("CEF initialized successfully");
|
tracing::info!("CEF initialized successfully");
|
||||||
context
|
context
|
||||||
|
|
@ -102,7 +104,7 @@ pub fn start() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli);
|
let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files);
|
||||||
|
|
||||||
let exit_reason = app.run(event_loop);
|
let exit_reason = app.run(event_loop);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ impl super::NativeWindow for NativeWindowImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self {
|
fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self {
|
||||||
|
app::setup(app_event_scheduler.clone());
|
||||||
let menu = menu::Menu::new(app_event_scheduler);
|
let menu = menu::Menu::new(app_event_scheduler);
|
||||||
|
|
||||||
NativeWindowImpl { menu }
|
NativeWindowImpl { menu }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
use objc2::{ClassType, define_class, msg_send};
|
use std::ffi::CStr;
|
||||||
use objc2_app_kit::{NSApplication, NSEvent, NSEventType, NSResponder};
|
use std::ffi::OsStr;
|
||||||
use objc2_foundation::NSObject;
|
use std::ops::Deref;
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Mutex, Once};
|
||||||
|
|
||||||
|
use objc2::rc::Retained;
|
||||||
|
use objc2::runtime::ProtocolObject;
|
||||||
|
use objc2::{ClassType, MainThreadMarker, MainThreadOnly, define_class, msg_send};
|
||||||
|
use objc2_app_kit::{NSApplication, NSApplicationDelegate, NSEvent, NSEventType, NSResponder};
|
||||||
|
use objc2_foundation::{NSArray, NSObject, NSObjectProtocol, NSURL};
|
||||||
|
|
||||||
|
use crate::event::{AppEvent, AppEventScheduler};
|
||||||
|
|
||||||
|
static APP_EVENT_SCHEDULER: Mutex<Option<AppEventScheduler>> = Mutex::new(None);
|
||||||
|
static INSTALL_DELEGATE: Once = Once::new();
|
||||||
|
|
||||||
|
static LAUNCH_DOCUMENTS: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
|
||||||
|
|
||||||
define_class!(
|
define_class!(
|
||||||
#[unsafe(super(NSApplication, NSResponder, NSObject))]
|
#[unsafe(super(NSApplication, NSResponder, NSObject))]
|
||||||
|
|
@ -20,12 +36,63 @@ define_class!(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
define_class!(
|
||||||
|
#[unsafe(super(NSObject))]
|
||||||
|
#[thread_kind = MainThreadOnly]
|
||||||
|
#[name = "GraphiteApplicationDelegate"]
|
||||||
|
struct GraphiteApplicationDelegate;
|
||||||
|
|
||||||
|
unsafe impl NSObjectProtocol for GraphiteApplicationDelegate {}
|
||||||
|
|
||||||
|
unsafe impl NSApplicationDelegate for GraphiteApplicationDelegate {
|
||||||
|
#[unsafe(method(application:openURLs:))]
|
||||||
|
fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
|
||||||
|
let app_event_scheduler = APP_EVENT_SCHEDULER.lock().unwrap();
|
||||||
|
|
||||||
|
let mut pending_paths_to_open = LAUNCH_DOCUMENTS.lock().unwrap();
|
||||||
|
|
||||||
|
for index in 0..urls.count() {
|
||||||
|
let url = urls.objectAtIndex(index);
|
||||||
|
if !url.isFileURL() {
|
||||||
|
tracing::error!("Ignoring macOS open URL event for non-file URL: {:?}", url);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = unsafe { CStr::from_ptr(url.fileSystemRepresentation().as_ptr()) };
|
||||||
|
let path = PathBuf::from(OsStr::from_bytes(path.to_bytes()));
|
||||||
|
|
||||||
|
pending_paths_to_open.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(app_event_scheduler) = app_event_scheduler.deref() {
|
||||||
|
app_event_scheduler.schedule(AppEvent::AddLaunchDocuments(std::mem::take(&mut pending_paths_to_open)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
fn instance() -> objc2::rc::Retained<NSApplication> {
|
fn instance() -> objc2::rc::Retained<NSApplication> {
|
||||||
unsafe { msg_send![GraphiteApplication::class(), sharedApplication] }
|
unsafe { msg_send![GraphiteApplication::class(), sharedApplication] }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn init() {
|
pub(super) fn init() {
|
||||||
let _ = instance();
|
let _ = instance();
|
||||||
|
|
||||||
|
INSTALL_DELEGATE.call_once(|| {
|
||||||
|
let mtm = MainThreadMarker::new().expect("only ever called from main thread");
|
||||||
|
let delegate: Retained<GraphiteApplicationDelegate> = unsafe { msg_send![super(GraphiteApplicationDelegate::alloc(mtm).set_ivars(())), init] };
|
||||||
|
instance().setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
|
||||||
|
std::mem::forget(delegate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn setup(app_event_scheduler: AppEventScheduler) {
|
||||||
|
let mut app_event_scheduler_guard = APP_EVENT_SCHEDULER.lock().unwrap();
|
||||||
|
|
||||||
|
let mut pending_paths_to_open = LAUNCH_DOCUMENTS.lock().unwrap();
|
||||||
|
app_event_scheduler.schedule(AppEvent::AddLaunchDocuments(std::mem::take(&mut pending_paths_to_open)));
|
||||||
|
|
||||||
|
*app_event_scheduler_guard = Some(app_event_scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn hide() {
|
pub(super) fn hide() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue