mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
Merge pull request #82 from storytold/fix/73-macos-open-from-finder
macOS: open PDFs from Finder, Open With and the Dock (#73)
This commit is contained in:
12 files changed
+157
-76
No files matched your search
Generated
+12
@@ -1787,6 +1787,17 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
||||
|
||||
[[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"
|
||||
@@ -3771,6 +3782,7 @@ version = "0.2.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"fmv-macos-events",
|
||||
"printcraft-ui-egui",
|
||||
"rustls-native-certs",
|
||||
"serde_json",
|
||||
|
||||
@@ -18,6 +18,12 @@ eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "
|
||||
egui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# Finder double-click / Open With / Dock drops arrive as Apple events (#73); this small audited
|
||||
# crate registers the handler (Objective-C interop needs unsafe, which our workspace forbids)
|
||||
# behind a safe API. Same version as PhotoCraft. Pinned exactly: review the diff before bumping.
|
||||
fmv-macos-events = "=0.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
# Icon + VERSIONINFO for printcraft.exe; build.rs only uses it when targeting Windows.
|
||||
winresource = "0.1"
|
||||
@@ -0,0 +1,46 @@
|
||||
//! macOS open-documents and quit Apple events (#73).
|
||||
//!
|
||||
//! Finder double-clicks, Open With, drops on the Dock icon and `open -a PrintCraft file.pdf` 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 AppKit answered "PrintCraft cannot open files in the PDF document
|
||||
//! format". 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) wraps exactly
|
||||
//! that, an `NSAppleEventManager` handler registered before Finder's launch event that leaves
|
||||
//! winit's delegate alone, behind a safe main-thread API. This module adapts it to the
|
||||
//! [`OsEvent`]s that the UI polls every frame.
|
||||
|
||||
use fmv_macos_events::{Event, Inbox, Registration};
|
||||
use printcraft_ui_egui::{OsEvent, OsEventsFn};
|
||||
|
||||
/// 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 UI drains (`PrintCraftApp::os_events`); events arriving later wake `ctx`.
|
||||
pub fn connect(&self, ctx: &egui::Context) -> OsEventsFn {
|
||||
let ctx = ctx.clone();
|
||||
self.inbox.set_wake(move || ctx.request_repaint());
|
||||
let inbox = self.inbox.clone();
|
||||
Box::new(move || {
|
||||
inbox
|
||||
.drain()
|
||||
.into_iter()
|
||||
.map(|e| match e {
|
||||
Event::Open(paths) => OsEvent::Open(paths.into_iter().map(|p| p.to_string_lossy().into_owned()).collect()),
|
||||
Event::Quit => OsEvent::Quit,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
use printcraft_ui_egui::PrintCraftApp;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod apple_events;
|
||||
mod updates;
|
||||
|
||||
/// Freedesktop app id: the `.desktop` file name and the hicolor icon name.
|
||||
@@ -79,6 +81,12 @@ fn main() -> eframe::Result {
|
||||
let persistence_path = eframe::storage_dir("PrintCraft").map(|d| d.join("app.ron"));
|
||||
let mut native = eframe::NativeOptions { viewport, persistence_path, ..Default::default() };
|
||||
configure_gpu(&mut native);
|
||||
// Finder, Open With and the Dock deliver files as Apple events, not arguments; catch the one
|
||||
// that launched us as well as later ones. Lives until the event loop returns.
|
||||
#[cfg(target_os = "macos")]
|
||||
let apple_events = apple_events::AppleEvents::install();
|
||||
#[cfg(target_os = "macos")]
|
||||
let apple_events = &apple_events;
|
||||
eframe::run_native(
|
||||
"PrintCraft",
|
||||
native,
|
||||
@@ -90,6 +98,10 @@ fn main() -> eframe::Result {
|
||||
app.integrated_titlebar = integrated;
|
||||
app.update_source = Some(std::sync::Arc::new(updates::latest_release));
|
||||
app.keychain_ids = cfg!(target_os = "macos");
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
app.os_events = Some(apple_events.connect(&cc.egui_ctx));
|
||||
}
|
||||
if let Some(file) = &control_file {
|
||||
let client = app.attach_control(&cc.egui_ctx);
|
||||
match printcraft_ui_egui::control::serve(client).and_then(|ep| write_control_file(file, ep.port, &ep.token).map(|()| ep.port)) {
|
||||
|
||||
@@ -253,6 +253,19 @@ pub struct PageClip {
|
||||
/// Files delivered asynchronously: (name, bytes).
|
||||
pub type Inbox = std::sync::Arc<std::sync::Mutex<Vec<(String, Vec<u8>)>>>;
|
||||
|
||||
/// A request the operating system sends the running app, outside its window: on macOS, Finder
|
||||
/// double-clicks, Open With and drops on the Dock icon arrive as Apple events, not arguments.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum OsEvent {
|
||||
/// Open these files.
|
||||
Open(Vec<String>),
|
||||
/// Quit (the Dock's Quit, logging out), asking about unsaved changes first.
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// Returns the [`OsEvent`]s that arrived since it was last called (set by the desktop app).
|
||||
pub type OsEventsFn = Box<dyn FnMut() -> Vec<OsEvent>>;
|
||||
|
||||
pub struct PasswordPrompt {
|
||||
pub name: String,
|
||||
pub path: Option<String>,
|
||||
@@ -300,6 +313,8 @@ pub struct PrintCraftApp {
|
||||
pub full_screen: bool,
|
||||
/// Files delivered asynchronously (web drag-and-drop, web file picker).
|
||||
pub inbox: Inbox,
|
||||
/// Requests from the operating system, polled every frame (macOS Apple events).
|
||||
pub os_events: Option<OsEventsFn>,
|
||||
/// A pending "save changes?" question (closing a dirty tab or quitting).
|
||||
pub close_request: Option<CloseRequest>,
|
||||
/// Save to this path instead of asking (tests and automation).
|
||||
@@ -470,6 +485,7 @@ impl PrintCraftApp {
|
||||
password_prompt: None,
|
||||
full_screen: false,
|
||||
inbox: Default::default(),
|
||||
os_events: None,
|
||||
close_request: None,
|
||||
save_override: None,
|
||||
props_draft: None,
|
||||
@@ -1141,6 +1157,17 @@ impl eframe::App for PrintCraftApp {
|
||||
self.notify(format!("Couldn't open {name}: {e}"));
|
||||
}
|
||||
}
|
||||
let os_events = self.os_events.as_mut().map(|poll| poll()).unwrap_or_default();
|
||||
for e in os_events {
|
||||
match e {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
OsEvent::Open(paths) => paths.iter().for_each(|p| self.open_path(p)),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
OsEvent::Open(_) => {}
|
||||
// Like closing the window: `guard_quit` asks about unsaved changes.
|
||||
OsEvent::Quit => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
|
||||
}
|
||||
}
|
||||
if let Some(mut control) = self.control.take() {
|
||||
control.tick(ctx, self);
|
||||
self.control = Some(control);
|
||||
|
||||
@@ -240,3 +240,23 @@ fn dropping_a_pdf_on_the_window_opens_it() {
|
||||
assert_eq!(h.state().views.len(), 1, "the dropped PDF opens in a tab");
|
||||
h.get_by_label_contains("dropped.pdf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_and_quit_from_the_operating_system() {
|
||||
// #73: macOS hands Finder double-clicks, Open With and Dock drops over as Apple events.
|
||||
use printcraft_ui_egui::OsEvent;
|
||||
let name = format!("printcraft-os-open-{}.pdf", std::process::id());
|
||||
let path = std::env::temp_dir().join(&name);
|
||||
std::fs::write(&path, FIXTURE).unwrap();
|
||||
let queue = std::rc::Rc::new(std::cell::RefCell::new(vec![OsEvent::Open(vec![path.to_string_lossy().into_owned()])]));
|
||||
let q = queue.clone();
|
||||
let mut h = harness(move |app| app.os_events = Some(Box::new(move || q.borrow_mut().drain(..).collect())));
|
||||
std::fs::remove_file(&path).ok();
|
||||
assert_eq!(h.state().views.len(), 1, "the file opens in a tab");
|
||||
h.get_by_label_contains(&name);
|
||||
// Quit closes the window like the close button, so unsaved changes are asked about.
|
||||
queue.borrow_mut().push(OsEvent::Quit);
|
||||
h.step();
|
||||
let closing = h.output().viewport_output.values().any(|v| v.commands.iter().any(|c| matches!(c, egui::ViewportCommand::Close)));
|
||||
assert!(closing);
|
||||
}
|
||||
@@ -12,7 +12,8 @@
|
||||
</developer>
|
||||
<description>
|
||||
<p>
|
||||
PrintCraft is a pdf editor. Read, organize, combine, split and secure PDFs
|
||||
PrintCraft is an open-source PDF workbench. Read, comment on, fill in and sign PDFs;
|
||||
organize, combine and split pages; edit, redact, protect and export documents.
|
||||
</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">ai.storyteller.printcraft.desktop</launchable>
|
||||
@@ -23,20 +24,19 @@
|
||||
<url type="help">https://github.com/storytold/printcraft</url>
|
||||
<url type="contact">https://discord.gg/artcraft</url>
|
||||
<categories>
|
||||
<category>Graphics</category>
|
||||
<category>RasterGraphics</category>
|
||||
<category>Photography</category>
|
||||
<category>Office</category>
|
||||
<category>Viewer</category>
|
||||
</categories>
|
||||
<keywords>
|
||||
<keyword>photo</keyword>
|
||||
<keyword>image editor</keyword>
|
||||
<keyword>psd</keyword>
|
||||
<keyword>layers</keyword>
|
||||
<keyword>pdf</keyword>
|
||||
<keyword>viewer</keyword>
|
||||
<keyword>annotate</keyword>
|
||||
<keyword>forms</keyword>
|
||||
</keywords>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>Editing The Great Wave off Kanagawa with type layers and adjustment layers</caption>
|
||||
<image>https://raw.githubusercontent.com/storytold/printcraft/main/docs/images/printcraft-demo.jpg</image>
|
||||
<caption>A PDF open with its threaded comments</caption>
|
||||
<image>https://raw.githubusercontent.com/storytold/printcraft/main/docs/images/printcraft-viewer.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<branding>
|
||||
@@ -53,6 +53,7 @@
|
||||
</supports>
|
||||
<provides>
|
||||
<binary>printcraft</binary>
|
||||
<mediatype>application/pdf</mediatype>
|
||||
</provides>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
|
||||
@@ -17,10 +17,9 @@ vendor: Learning Machines LLC
|
||||
homepage: https://getartcraft.com/apps/printcraft
|
||||
license: MIT OR Apache-2.0
|
||||
description: |-
|
||||
Open-source, native image editor that works the way Photoshop users expect.
|
||||
Layers, masks, adjustment layers, layer styles, type and brushes; opens and
|
||||
saves layered PSD/PSB files; 8/16/32-bit documents; GPU compositing (wgpu).
|
||||
Includes printcraft-cli, a headless converter and command runner.
|
||||
Open-source, native PDF workbench: read, comment, fill and sign forms,
|
||||
organize, combine, split, edit, redact, protect and export PDFs.
|
||||
Includes printcraft-cli, a headless command runner and opt-in MCP server.
|
||||
|
||||
# The binaries link only glibc and libgcc_s directly. Windowing (X11/Wayland, xkbcommon) and the
|
||||
# GPU (Vulkan, EGL/GL) are loaded at runtime, so they are dependencies/recommendations here
|
||||
|
||||
@@ -45,64 +45,23 @@
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © the PrintCraft authors. MIT OR Apache-2.0.</string>
|
||||
|
||||
<!-- Printcraft's own format. -->
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<!-- Documents PrintCraft opens (#73): it is offered under Open With and can be made the default
|
||||
for PDFs, without taking over from the system's viewer on install ("Alternate"). Declared by
|
||||
extension and MIME type; LaunchServices maps them to the system's PDF type. Finder hands the
|
||||
file over as an Apple event, which apps/printcraft/src/apple_events.rs receives. -->
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>ai.storyteller.printcraft.document</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>PrintCraft Document</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.image</string>
|
||||
</array>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>PrintCraft</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array><string>pcraft</string></array>
|
||||
<key>public.mime-type</key>
|
||||
<array><string>application/x-printcraft</string></array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- Types macOS may not know; the system's own declaration wins when it has one. -->
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.adobe.photoshop-large-image</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Photoshop Large Document</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.image</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array><string>psb</string></array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>ai.storyteller.printcraft.qoi</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>QOI Image</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.image</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array><string>qoi</string></array>
|
||||
</dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>PDF Document</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array><string>pdf</string></array>
|
||||
<key>CFBundleTypeMIMETypes</key>
|
||||
<array><string>application/pdf</string></array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Local test: `python3 -m http.server 8765` inside the folder, then open http://lo
|
||||
```html
|
||||
<iframe
|
||||
src="https://example.com/printcraft/"
|
||||
title="PrintCraft image editor"
|
||||
title="PrintCraft PDF workbench"
|
||||
style="width: 100%; height: 720px; border: 0;"
|
||||
allow="fullscreen; clipboard-read; clipboard-write"
|
||||
allowfullscreen>
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
|
||||
wix build printcraft.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
|
||||
|
||||
Per-machine install into Program Files, a Start Menu shortcut, `.pcraft` owned by PrintCraft,
|
||||
and PrintCraft offered under "Open with" for PSD/PSB and the image formats it reads (without
|
||||
taking over the user's default app for them).
|
||||
Per-machine install into Program Files, a Start Menu shortcut and an App Paths entry. Files
|
||||
opened with PrintCraft arrive on its command line.
|
||||
-->
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Package
|
||||
|
||||
@@ -22,8 +22,8 @@ milestone = "M1"
|
||||
status = "shipped"
|
||||
commands = ["file.open"]
|
||||
tools = ["doc_open"]
|
||||
evidence = ["crates/automation/tests/automation.rs::open_inspect_render_and_find", "crates/ui-egui/tests/ui.rs::opening_a_pdf_shows_comments_and_bookmarks"]
|
||||
notes = ""
|
||||
evidence = ["crates/automation/tests/automation.rs::open_inspect_render_and_find", "crates/ui-egui/tests/ui.rs::opening_a_pdf_shows_comments_and_bookmarks", "crates/ui-egui/tests/ui.rs::files_and_quit_from_the_operating_system"]
|
||||
notes = "File ▸ Open, the command line (Linux and Windows file associations), and on macOS Finder, Open With and the Dock (Apple events; the bundle declares PDF as a document type)"
|
||||
|
||||
[[feature]]
|
||||
id = "core.repair-broken-files"
|
||||
|
||||
Reference in new issue
Block a user