mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
macOS: projects open from Finder; .pcraft is PhotoCraft's again
The app bundle exported ai.storyteller.effectcraft.document with the
`.pcraft` extension, PhotoCraft's document format. Installing EffectCraft
took `.pcraft` over, and since EffectCraft declared no document types,
neither app was offered for PhotoCraft documents in Finder. EffectCraft's
own projects (.ecproj, .ecprojx) had no file type at all.
- Info.plist: the exported type now tags .ecproj and .ecprojx ("EffectCraft
Project", conforming to public.data and public.content rather than
public.image), and CFBundleDocumentTypes makes EffectCraft their owner.
- Finder double-click, Open With, Dock drops and `open -a EffectCraft
x.ecproj` send a kAEOpenDocuments Apple event that winit 0.30 doesn't
handle. Register it with the audited fmv-macos-events crate that
PhotoCraft and PdfCraft use (it keeps the unsafe Objective-C interop out
of this workspace). Opened paths go through the dropped-file handling, so
a project opens after the unsaved-changes prompt and media is imported. A
quit event closes the window.
- The event that launches the app arrives before the UI draws: the first
frames skip the panels until the fonts are installed, which dropped it.
The paths wait for EffectcraftApp::is_ready().
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
813c7c4650
commit
f041aa7782
6 files changed
+141
-4
No files matched your search
Generated
+12
@@ -1142,6 +1142,7 @@ dependencies = [
|
||||
"effectcraft-ui-egui",
|
||||
"eframe",
|
||||
"egui",
|
||||
"fmv-macos-events",
|
||||
"log",
|
||||
"muda",
|
||||
"objc2 0.6.4",
|
||||
@@ -2280,6 +2281,17 @@ dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fmv-macos-events"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90642cf66dde7f205dc8b42dfe57b47d0f3a29f571b832742883ef512e23fe55"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
|
||||
@@ -35,3 +35,8 @@ muda = { version = "0.21", default-features = false }
|
||||
objc2 = "0.6"
|
||||
# NSApplication hide / hide others / show all, and the pasteboard for Edit ▸ Paste in text fields.
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSPasteboard"] }
|
||||
# Finder double-click, Open With, Dock drops and `open -a EffectCraft x.ecproj` arrive as a
|
||||
# kAEOpenDocuments Apple event, which winit 0.30 doesn't handle. This audited crate (also used by
|
||||
# PhotoCraft and PdfCraft) registers the handler (Objective-C interop needs unsafe, which our
|
||||
# workspace forbids) behind a safe API. Pinned exactly: review the diff before bumping.
|
||||
fmv-macos-events = "=0.1.0"
|
||||
@@ -0,0 +1,80 @@
|
||||
//! macOS open-documents and quit Apple events.
|
||||
//!
|
||||
//! Finder double-clicks, Open With, drops on the Dock icon and `open -a EffectCraft x.ecproj` don't
|
||||
//! pass paths on the command line: LaunchServices sends the running (or just-launched) app a
|
||||
//! `kAEOpenDocuments` ('odoc') Apple event. winit 0.30 doesn't handle it and owns the
|
||||
//! `NSApplicationDelegate`, so the project never opened. Handling it ourselves needs Objective-C
|
||||
//! class declarations, i.e. `unsafe`, which this workspace forbids. The audited `fmv-macos-events`
|
||||
//! crate (also used by PhotoCraft and PdfCraft) wraps exactly that, an `NSAppleEventManager`
|
||||
//! handler registered before Finder's launch event that leaves winit's delegate alone, behind a
|
||||
//! safe main-thread API.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use fmv_macos_events::{Event, Inbox, Registration};
|
||||
|
||||
/// Keeps the Apple-event handlers registered; hold it until the event loop returns.
|
||||
pub struct AppleEvents {
|
||||
_registration: Registration,
|
||||
inbox: Inbox,
|
||||
}
|
||||
|
||||
impl AppleEvents {
|
||||
/// Register the handlers. Call on the main thread before the event loop starts, so the event
|
||||
/// that launched the app (a Finder double-click) is caught too.
|
||||
pub fn install() -> Self {
|
||||
let (registration, inbox) = Registration::install();
|
||||
Self { _registration: registration, inbox }
|
||||
}
|
||||
|
||||
/// The queue the app drains every frame ([`Opened::feed`]); events arriving later wake `ctx`.
|
||||
pub fn connect(&self, ctx: &egui::Context) -> Opened {
|
||||
let ctx = ctx.clone();
|
||||
self.inbox.set_wake(move || ctx.request_repaint());
|
||||
Opened { inbox: self.inbox.clone(), pending: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Documents macOS asked us to open, waiting for the UI to take them.
|
||||
pub struct Opened {
|
||||
inbox: Inbox,
|
||||
/// Paths that arrived before the UI was ready (the Finder event that launched the app comes
|
||||
/// before the first frame is drawn).
|
||||
pending: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Opened {
|
||||
/// Hand the documents to the dropped-file handling once the UI is `ready` (a project opens,
|
||||
/// asking about unsaved changes first; media is imported), and act on quit requests.
|
||||
pub fn feed(&mut self, ready: bool, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
for e in self.inbox.drain() {
|
||||
match e {
|
||||
Event::Open(paths) => self.pending.extend(paths),
|
||||
Event::Quit => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
|
||||
}
|
||||
}
|
||||
if self.pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !ready {
|
||||
ctx.request_repaint();
|
||||
return;
|
||||
}
|
||||
raw.dropped_files.extend(self.pending.drain(..).map(|p| Arc::new(OpenedFile(p)) as egui::DroppedFileHandle));
|
||||
}
|
||||
}
|
||||
|
||||
/// A document macOS asked us to open, presented to egui as a dropped file.
|
||||
#[derive(Debug)]
|
||||
struct OpenedFile(PathBuf);
|
||||
|
||||
impl egui::DroppedFile for OpenedFile {
|
||||
fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn bytes(&self) -> Result<Vec<u8>, String> {
|
||||
std::fs::read(&self.0).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@
|
||||
// Built everywhere so its tests run on every platform; only Linux AppImages use it.
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
mod appimage;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod apple_events;
|
||||
mod audio_out;
|
||||
mod control_server;
|
||||
mod launch_guard;
|
||||
@@ -101,6 +103,12 @@ fn main() -> eframe::Result {
|
||||
// Start-up milestones: with `RUST_LOG=info` they go to stderr, so a window that never
|
||||
// appears shows how far start-up got (#234).
|
||||
log::info!("effectcraft {}: opening the window", env!("CARGO_PKG_VERSION"));
|
||||
// Registered before the event loop starts, so it catches the Finder event that launched us as
|
||||
// well as later ones. Lives until the event loop returns; the app creator only borrows it.
|
||||
#[cfg(target_os = "macos")]
|
||||
let apple_events = apple_events::AppleEvents::install();
|
||||
#[cfg(target_os = "macos")]
|
||||
let apple_events = &apple_events;
|
||||
let result = eframe::run_native(
|
||||
"EffectCraft",
|
||||
options,
|
||||
@@ -207,6 +215,8 @@ fn main() -> eframe::Result {
|
||||
Ok(Box::new(Desktop {
|
||||
#[cfg(target_os = "macos")]
|
||||
menu: native_menu::NativeBar::new(&cc.egui_ctx),
|
||||
#[cfg(target_os = "macos")]
|
||||
apple_events: apple_events.connect(&cc.egui_ctx),
|
||||
app,
|
||||
launch,
|
||||
drawn: false,
|
||||
@@ -274,6 +284,9 @@ struct Desktop {
|
||||
app: EffectcraftApp,
|
||||
#[cfg(target_os = "macos")]
|
||||
menu: native_menu::NativeBar,
|
||||
/// Documents opened from Finder, the Dock or `open` (macOS).
|
||||
#[cfg(target_os = "macos")]
|
||||
apple_events: apple_events::Opened,
|
||||
launch: launch_guard::Launch,
|
||||
/// The first frame was drawn (logged once).
|
||||
drawn: bool,
|
||||
@@ -300,6 +313,8 @@ impl eframe::App for Desktop {
|
||||
}
|
||||
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
|
||||
#[cfg(target_os = "macos")]
|
||||
self.apple_events.feed(self.app.is_ready(), ctx, raw_input);
|
||||
self.app.raw_input_hook(ctx, raw_input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,6 +510,12 @@ impl EffectcraftApp {
|
||||
&& self.ui.viewer.extended.is_none()
|
||||
}
|
||||
|
||||
/// The UI draws its panels and handles input such as dropped files. Until the fonts are
|
||||
/// installed (the first frames) it draws nothing, and that input goes unhandled.
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.fonts_ready
|
||||
}
|
||||
|
||||
/// Provide the executable/embedding host's device notifications. The host installs the
|
||||
/// handlers once; this frontend never replaces handlers on a borrowed device.
|
||||
pub fn set_gpu_failure_bridge(&mut self, bridge: gpu_failure::GpuFailureBridge) {
|
||||
|
||||
@@ -45,25 +45,44 @@
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © the EffectCraft authors. MIT OR Apache-2.0.</string>
|
||||
|
||||
<!-- Effectcraft's own format. -->
|
||||
<!-- Projects open from Finder (double-click, Open With, Dock); src/apple_events.rs. -->
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>EffectCraft Project</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>EffectCraft</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array><string>ai.storyteller.effectcraft.document</string></array>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- EffectCraft's own format: the project (.ecproj) and its XML copy (.ecprojx). -->
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>ai.storyteller.effectcraft.document</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>EffectCraft Document</string>
|
||||
<string>EffectCraft Project</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.image</string>
|
||||
<string>public.content</string>
|
||||
</array>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>EffectCraft</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array><string>pcraft</string></array>
|
||||
<array>
|
||||
<string>ecproj</string>
|
||||
<string>ecprojx</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array><string>application/x-effectcraft</string></array>
|
||||
</dict>
|
||||
|
||||
Reference in new issue
Block a user