diff --git a/apps/vectorcraft/src/host_services.rs b/apps/vectorcraft/src/host_services.rs index e25332b61..81a20e601 100644 --- a/apps/vectorcraft/src/host_services.rs +++ b/apps/vectorcraft/src/host_services.rs @@ -300,6 +300,8 @@ pub(crate) fn services(parent: Parent, ui: egui::Context) -> Services { reveal: Some(Box::new(reveal)), // Links panel: Edit Original; Package: Show Package. Relink to Folder and Package pick folders. open_file: Some(Box::new(open_file)), + // Edit Original of a raster image with Use System Defaults for Edit Original off (#5236). + open_in_photocraft: Some(Box::new(open_in_photocraft)), pick_folder: Some(Box::new(move || pick_now(PickRequest::Folder, &p4).into_iter().next())), // Sample Color from Screen: the system's color sampler. #[cfg(target_os = "macos")] @@ -334,3 +336,281 @@ fn open_file(path: &str) -> Result<(), String> { c.arg(path); c.spawn().map(|_| ()).map_err(|e| format!("can't open {path}: {e}")) } + +/// Edit Original with Preferences › File Handling › Use System Defaults for Edit Original off +/// (#5236): open the raster image `path` in PhotoCraft, found only where it installs +/// ([`find_photocraft`]); `Ok(false)` when it isn't installed. PhotoCraft gets the path as its only +/// argument and is never started through a shell. +fn open_in_photocraft(path: &str) -> Result { + explorer_safe(path)?; + let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).map(std::path::PathBuf::from); + let Some(app) = find_photocraft(home.as_deref(), std::env::var_os("PATH").as_deref()) else { return Ok(false) }; + #[cfg(target_os = "macos")] + let mut c = { + let mut c = std::process::Command::new("open"); + c.arg("-a").arg(&app).arg(path); + c + }; + #[cfg(not(target_os = "macos"))] + let mut c = { + let mut c = std::process::Command::new(&app); + c.arg(path); + c + }; + // macOS: `open` returns once Launch Services has the request, so a short wait reports an app + // it can't open (a folder named `.app` that isn't a bundle). The wait is capped so a stalled + // `open` never holds the window: past it, the launch is left to run. Elsewhere the editor + // itself runs: waiting would last until it closes. + #[cfg(target_os = "macos")] + { + const WAIT: std::time::Duration = std::time::Duration::from_secs(2); + let fail = |why: String| format!("can't open {path} in {}: {why}", app.display()); + let mut child = c.stderr(std::process::Stdio::piped()).spawn().map_err(|e| fail(e.to_string()))?; + let started = std::time::Instant::now(); + loop { + match child.try_wait() { + Ok(Some(status)) if status.success() => return Ok(true), + Ok(Some(status)) => { + let mut why = String::new(); + if let Some(mut err) = child.stderr.take() { + // Best effort: without its message the exit status says why. + let _ = std::io::Read::read_to_string(&mut err, &mut why); + } + let why = why.trim().to_string(); + return Err(fail(if why.is_empty() { status.to_string() } else { why })); + } + Ok(None) if started.elapsed() < WAIT => std::thread::sleep(std::time::Duration::from_millis(20)), + Ok(None) => { + // Reaped off the UI thread once it ends. + std::thread::spawn(move || { + let _ = child.wait(); + }); + return Ok(true); + } + Err(e) => return Err(fail(e.to_string())), + } + } + } + #[cfg(not(target_os = "macos"))] + c.spawn().map(|_| true).map_err(|e| format!("can't open {path} in {}: {e}", app.display())) +} + +/// PhotoCraft where it is installed: the first of [`photocraft_candidates`] that is an application. +pub(crate) fn find_photocraft(home: Option<&std::path::Path>, path_var: Option<&std::ffi::OsStr>) -> Option { + photocraft_candidates(home, path_var).into_iter().find(|p| check_app(p).is_ok()) +} + +/// Where PhotoCraft may be installed: the Applications folders on macOS; on Windows where its +/// installer puts it ([`windows_candidates`]); else a `photocraft` program on the `PATH`. +pub(crate) fn photocraft_candidates(home: Option<&std::path::Path>, path_var: Option<&std::ffi::OsStr>) -> Vec { + if cfg!(target_os = "macos") { + let mut v = vec![std::path::PathBuf::from("/Applications/PhotoCraft.app")]; + v.extend(home.map(|h| h.join("Applications/PhotoCraft.app"))); + return v; + } + let name = if cfg!(windows) { "photocraft.exe" } else { "photocraft" }; + let on_path = path_var.map(|p| std::env::split_paths(p).map(|dir| dir.join(name)).collect()).unwrap_or_default(); + if cfg!(windows) { + let folders: Vec = [ + std::env::var_os("LOCALAPPDATA").map(|d| std::path::PathBuf::from(d).join("Programs")), + std::env::var_os("ProgramW6432").map(std::path::PathBuf::from), + std::env::var_os("ProgramFiles").map(std::path::PathBuf::from), + ] + .into_iter() + .flatten() + .collect(); + return windows_candidates(registered_photocraft(), &folders, on_path); + } + on_path +} + +/// Windows: PhotoCraft's installer puts it in `ArtCraft\PhotoCraft` under a folder of +/// `program_folders` (for the current user `%LOCALAPPDATA%\Programs`, for everyone Program Files) +/// and registers its path under App Paths, which also covers a folder chosen in setup. So: the +/// `registered` paths first, then those folders, then `on_path`. +pub(crate) fn windows_candidates( + registered: Vec, + program_folders: &[std::path::PathBuf], + on_path: Vec, +) -> Vec { + let mut v = registered; + for f in program_folders { + let p = f.join("ArtCraft").join("PhotoCraft").join("photocraft.exe"); + if !v.contains(&p) { + v.push(p); + } + } + v.extend(on_path); + v +} + +/// The paths PhotoCraft's installer registers under App Paths, for the current user and then for +/// everyone (`reg query`, so no extra dependency; a hidden console window). +#[cfg(windows)] +fn registered_photocraft() -> Vec { + use std::os::windows::process::CommandExt as _; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + // reg.exe by its full path, so nothing named `reg` in the app's folder or on PATH runs instead. + let root = std::env::var_os("SystemRoot").map(std::path::PathBuf::from).unwrap_or_else(|| r"C:\Windows".into()); + ["HKCU", "HKLM"] + .into_iter() + .filter_map(|hive| { + let key = format!(r"{hive}\Software\Microsoft\Windows\CurrentVersion\App Paths\photocraft.exe"); + let out = std::process::Command::new(root.join("System32").join("reg.exe")) + .args(["query", &key, "/ve"]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .ok()?; + registered_path(&String::from_utf8_lossy(&out.stdout)) + }) + .collect() +} + +#[cfg(not(windows))] +fn registered_photocraft() -> Vec { + Vec::new() +} + +/// The path in `reg query … /ve` output like +/// ` (Default) REG_SZ C:\Program Files\ArtCraft\PhotoCraft\photocraft.exe`. +#[cfg_attr(not(any(windows, test)), allow(dead_code))] +pub(crate) fn registered_path(text: &str) -> Option { + text.lines().find_map(|l| l.split_once("REG_SZ")).map(|(_, v)| v.trim().trim_matches('"')).filter(|v| !v.is_empty()).map(std::path::PathBuf::from) +} + +/// Is `app` an application Edit Original may start: an `.app` bundle on macOS, an `.exe` on +/// Windows, an executable file elsewhere? Anything else (a document, a folder, a script on +/// Windows) found where PhotoCraft installs is not taken for it. +pub(crate) fn check_app(app: &std::path::Path) -> Result<(), String> { + let ext = app.extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default(); + let ok = if cfg!(target_os = "macos") { + ext == "app" && app.is_dir() + } else if cfg!(windows) { + ext == "exe" && app.is_file() + } else { + executable_file(app) + }; + if ok { Ok(()) } else { Err(format!("{} isn't an application Edit Original can open files in", app.display())) } +} + +#[cfg(unix)] +fn executable_file(p: &std::path::Path) -> bool { + use std::os::unix::fs::PermissionsExt as _; + std::fs::metadata(p).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0) +} + +#[cfg(not(unix))] +fn executable_file(p: &std::path::Path) -> bool { + p.is_file() +} + +#[cfg(test)] +mod open_with_tests { + use super::{check_app, find_photocraft, photocraft_candidates, registered_path, windows_candidates}; + + /// A fresh, empty folder under the system's temporary folder. + fn scratch(name: &str) -> std::path::PathBuf { + let d = std::env::temp_dir().join(format!("vc-open-with-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d + } + + /// #5236: only an application counts as PhotoCraft, never a document, a folder or a missing + /// path. + #[test] + fn only_applications_are_started() { + let d = scratch("check"); + let doc = d.join("notes.txt"); + std::fs::write(&doc, "hi").unwrap(); + assert!(check_app(&doc).is_err(), "a document"); + assert!(check_app(&d).is_err(), "a folder"); + assert!(check_app(&d.join("Missing.app")).is_err(), "a missing path"); + if cfg!(target_os = "macos") { + let app = d.join("Editor.app"); + std::fs::create_dir_all(&app).unwrap(); + assert!(check_app(&app).is_ok(), "an .app bundle"); + } + #[cfg(all(unix, not(target_os = "macos")))] + { + use std::os::unix::fs::PermissionsExt as _; + let tool = d.join("editor"); + std::fs::write(&tool, "#!/bin/sh\n").unwrap(); + assert!(check_app(&tool).is_err(), "not executable yet"); + std::fs::set_permissions(&tool, std::fs::Permissions::from_mode(0o755)).unwrap(); + assert!(check_app(&tool).is_ok(), "an executable file"); + } + let _ = std::fs::remove_dir_all(&d); + } + + /// PhotoCraft is looked for only where it installs. + #[test] + fn photocraft_is_found_where_it_installs() { + let d = scratch("find"); + if cfg!(target_os = "macos") { + let candidates = photocraft_candidates(Some(&d), None); + assert_eq!(candidates.first().map(|p| p.to_string_lossy().into_owned()).as_deref(), Some("/Applications/PhotoCraft.app")); + assert!(candidates.contains(&d.join("Applications/PhotoCraft.app")), "{candidates:?}"); + std::fs::create_dir_all(d.join("Applications/PhotoCraft.app")).unwrap(); + assert!(find_photocraft(Some(&d), None).unwrap().ends_with("PhotoCraft.app")); + } else { + let bin = d.join("bin"); + std::fs::create_dir_all(&bin).unwrap(); + let path_var = std::env::join_paths([&bin]).unwrap(); + // On Windows an installed PhotoCraft (registered, or in a program folder) comes first, so + // there only the `PATH` candidate is checked. + if !cfg!(windows) { + assert!(find_photocraft(None, Some(&path_var)).is_none(), "not installed"); + } + let exe = bin.join(if cfg!(windows) { "photocraft.exe" } else { "photocraft" }); + std::fs::write(&exe, "").unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + if cfg!(windows) { + assert_eq!(photocraft_candidates(None, Some(&path_var)).last(), Some(&exe)); + assert!(check_app(&exe).is_ok()); + } else { + assert_eq!(find_photocraft(None, Some(&path_var)), Some(exe)); + } + } + let _ = std::fs::remove_dir_all(&d); + } + + /// #5327 review: on Windows PhotoCraft is looked for where its installer puts it: the path it + /// registers, then `ArtCraft\PhotoCraft` in the per-user and per-machine program folders, then + /// the `PATH`. + #[test] + fn windows_looks_where_the_photocraft_installer_puts_it() { + use std::path::PathBuf; + let registered = vec![PathBuf::from("D:/Tools/PhotoCraft/photocraft.exe")]; + let folders = [PathBuf::from("C:/Users/me/AppData/Local/Programs"), PathBuf::from("C:/Program Files"), PathBuf::from("C:/Program Files")]; + let on_path = vec![PathBuf::from("C:/bin/photocraft.exe")]; + let exe = |f: &str| PathBuf::from(f).join("ArtCraft").join("PhotoCraft").join("photocraft.exe"); + assert_eq!( + windows_candidates(registered, &folders, on_path), + [ + PathBuf::from("D:/Tools/PhotoCraft/photocraft.exe"), + exe("C:/Users/me/AppData/Local/Programs"), + exe("C:/Program Files"), + PathBuf::from("C:/bin/photocraft.exe"), + ] + ); + } + + /// The registered path is read from `reg query … /ve` output, spaces and quotes included; + /// no value, no path. + #[test] + fn the_registered_path_is_read_from_reg_query() { + let out = "\r\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\photocraft.exe\r\n (Default) REG_SZ C:\\Program Files\\ArtCraft\\PhotoCraft\\photocraft.exe\r\n"; + assert_eq!(registered_path(out), Some(std::path::PathBuf::from(r"C:\Program Files\ArtCraft\PhotoCraft\photocraft.exe"))); + assert_eq!( + registered_path(" (Standard) REG_SZ \"C:\\A B\\photocraft.exe\""), + Some(std::path::PathBuf::from(r"C:\A B\photocraft.exe")) + ); + assert_eq!(registered_path("ERROR: The system was unable to find the specified registry key or value."), None); + assert_eq!(registered_path(" (Default) REG_SZ "), None); + } +} diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index 6950eeaac..ad24afacb 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -372,6 +372,8 @@ pub const PREF_SPECS: &[PrefSpec] = &[ p!("newArtBasic", "General", "Appearance Panel", "New Art Has Basic Appearance", bool), // File Handling (continued) p!("templatesFolder", "File Handling", "Files", "Templates Folder", text), + // Off: raster images open in PhotoCraft where it is installed (#5236). + p!("editOriginalSystemDefaults", "File Handling", "Files", "Use System Defaults for Edit Original", bool), p!("appendConverted", "File Handling", "Files", "Mark Older Files as [Converted] When Opened", bool), // Native saves (`document.save {compress}`) p!("useCompression", "File Handling", "Files", "Use Compression", bool), diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index db3ee7e75..5deb00ddd 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -253,6 +253,10 @@ pub struct Prefs { // File Handling (continued) /// Where Save as Template and New from Template start ("" = `Documents/VectorCraft Templates`). pub templates_folder: String, + /// File Handling → Use System Defaults for Edit Original (#5236): Links panel › Edit Original + /// opens a linked file in the system's default app for it; off, in the app that made it + /// (PhotoCraft for a raster image, where it is installed; the system's app otherwise). + pub edit_original_system_defaults: bool, /// Open older native files as " [Converted]" so Save asks for a new name. pub append_converted: bool, /// File Handling → Use Compression: native saves are gzip-compressed (`document.save @@ -464,6 +468,7 @@ impl Default for Prefs { pdf_presets: vec![], screen_export_presets: vec![], templates_folder: String::new(), + edit_original_system_defaults: true, append_converted: true, use_compression: false, web_export_presets: vec![], diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index df04d2843..b56d79bc6 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1799,6 +1799,7 @@ font-technology Variable Променливи Manually Ръчно Mark as trapped Маркиране като прехванато Mark Older Files as [Converted] When Opened Маркиране на по-стари файлове като [Конвертирани] при отваряне + Use System Defaults for Edit Original Системни настройки по подразбиране за „Редактиране на оригинал“ Marker… Маркер… Marks Маркери Marks and Bleed Маркери и изтичане diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 95c7fadfa..0e2af6d29 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -3560,6 +3560,7 @@ theme Appearance Izgled List Preferences Izlistaj postavke Manually Ručno Mark Older Files as [Converted] When Opened Označi starije datoteke kao [Pretvoreno] pri otvaranju + Use System Defaults for Edit Original Koristi sistemske zadane postavke za Uredi original Match User Interface Brightness Uskladi sa svjetlinom korisničkog interfejsa Light Gray Svijetlosiva Medium Gray Srednje siva diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index b656559e4..c3b297439 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -2330,6 +2330,7 @@ digits Malayalam Malaiàlam Manually Manualment Mark as trapped Marca com a reventat Mark Older Files as [Converted] When Opened Marca els fitxers antics com a [Convertit] en obrir-los + Use System Defaults for Edit Original Utilitza els valors per defecte del sistema per a Edita l’original Marker… Marcador… Marks Marques Marks and Bleed Marques i sangrat diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index fc7698da6..985829f34 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3601,6 +3601,7 @@ theme Appearance Motiv List Preferences Vypsat předvolby Manually Ručně Mark Older Files as [Converted] When Opened Při otevření označit starší soubory jako [Converted] + Use System Defaults for Edit Original Pro Upravit originál použít výchozí aplikace systému Match User Interface Brightness Odpovídat jasu uživatelského rozhraní Light Gray Světle šedá Medium Gray Středně šedá diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index c8a92d7d3..a82b470a5 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -3328,6 +3328,7 @@ theme Appearance Udseende List Preferences Oplist indstillinger Manually Manuelt Mark Older Files as [Converted] When Opened Markér ældre filer som [konverteret] ved åbning + Use System Defaults for Edit Original Anvend systemets standarder til Redigér original Match User Interface Brightness Match lysstyrken i brugergrænsefladen Light Gray Lysegrå Medium Gray Mellemgrå diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 1504af382..c36884743 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1861,6 +1861,7 @@ file-filter Templates Vorlagen Manually Manuell Mark as trapped Als überfüllt kennzeichnen Mark Older Files as [Converted] When Opened Ältere Dateien beim Öffnen als [Konvertiert] kennzeichnen + Use System Defaults for Edit Original Systemstandards für „Original bearbeiten“ verwenden Marker… Markierung… Marks Marken Marks and Bleed Marken und Anschnitt diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index de8b95f0a..bfae8c355 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2651,6 +2651,7 @@ theme Appearance Εμφάνιση Manually Χειροκίνητα Mark as trapped Σήμανση ως επικαλυπτόμενου Mark Older Files as [Converted] When Opened Σήμανση παλαιότερων αρχείων ως [Μετατράπηκε] κατά το άνοιγμα + Use System Defaults for Edit Original Χρήση προεπιλογών συστήματος για την Επεξεργασία πρωτοτύπου Marker… Δείκτης… Marks Σημάδια Marks and Bleed Σημάδια και ξάκρισμα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ccf04f1f4..62c5bbccd 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1864,6 +1864,7 @@ font-technology Variable Variables Manually Manualmente Mark as trapped Marcar como reventado Mark Older Files as [Converted] When Opened Marcar archivos antiguos como [Convertido] al abrirlos + Use System Defaults for Edit Original Usar valores predeterminados del sistema para Editar original Marker… Marcador… Marks Marcas Marks and Bleed Marcas y sangrado diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 048efe166..9586138d6 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -3525,6 +3525,7 @@ theme Appearance Ulkoasu List Preferences Luettele asetukset Manually Manuaalisesti Mark Older Files as [Converted] When Opened Merkitse vanhemmat tiedostot [Muunnettu] avattaessa + Use System Defaults for Edit Original Käytä järjestelmän oletuksia toiminnossa Muokkaa alkuperäistä Match User Interface Brightness Sovita käyttöliittymän kirkkauteen Light Gray Vaaleanharmaa Medium Gray Keskiharmaa diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a42c01e8d..d0003abb0 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1858,6 +1858,7 @@ font-technology Variable Variables Manually Manuellement Mark as trapped Marquer comme grossi-maigri Mark Older Files as [Converted] When Opened Marquer les anciens fichiers comme [Converti] à l’ouverture + Use System Defaults for Edit Original Utiliser les applications par défaut du système pour Modifier l’original Marker… Marqueur… Marks Repères Marks and Bleed Repères et fond perdu diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 12e8b0ec1..e3fee02cb 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2351,6 +2351,7 @@ file-filter Templates Sablonok Manually Kézzel Mark as trapped Megjelölés csapdázottként Mark Older Files as [Converted] When Opened Régebbi fájlok megjelölése [Átalakítva] címkével megnyitáskor + Use System Defaults for Edit Original Rendszer alapértelmezéseinek használata az Eredeti szerkesztéséhez Marker… Jelölő… Marks Jelek Marks and Bleed Jelek és kifutó diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 19d46d86c..cf0e676a5 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -3328,6 +3328,7 @@ theme Appearance Tampilan List Preferences Daftarkan preferensi Manually Secara manual Mark Older Files as [Converted] When Opened Tandai file lama sebagai [Converted] saat dibuka + Use System Defaults for Edit Original Gunakan default sistem untuk Edit asli Match User Interface Brightness Sesuaikan dengan kecerahan antarmuka pengguna Light Gray Abu-abu terang Medium Gray Abu-abu sedang diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 6ff9da125..26c53586e 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1893,6 +1893,7 @@ file-filter Templates Modelli Manually Manualmente Mark as trapped Contrassegna come sottoposto a trapping Mark Older Files as [Converted] When Opened Contrassegna i file meno recenti come [Convertito] all'apertura + Use System Defaults for Edit Original Usa le impostazioni predefinite di sistema per Modifica originale Marker… Indicatore… Marks Segni Marks and Bleed Segni e abbondanza diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 53c779802..74404bb57 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2094,6 +2094,7 @@ digits Malayalam マラヤーラム数字 Manage Workspaces ワークスペースを管理 Manually 手動 Mark Older Files as [Converted] When Opened 古いファイルを開くときに[変換]と表示 + Use System Defaults for Edit Original 「オリジナルを編集」にシステムのデフォルトを使用 Mark as trapped トラップ済みとしてマーク Marks トンボ Match User Interface Brightness ユーザーインターフェイスの明るさに合わせる diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 937db59f0..ea9b2d73a 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -3138,6 +3138,7 @@ theme Appearance იერსახე List Preferences პარამეტრების სია Manually ხელით Mark Older Files as [Converted] When Opened ძველი ფაილების მარკირება როგორც [გარდაქმნილი] გახსნისას + Use System Defaults for Edit Original სისტემის ნაგულისხმევის გამოყენება ორიგინალის რედაქტირებისთვის Match User Interface Brightness ინტერფეისის სიკაშკაშესთან შესაბამისობა Light Gray ღია ნაცრისფერი Medium Gray საშუალო ნაცრისფერი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 550556091..a94da4fea 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2084,6 +2084,7 @@ digits Malayalam 말라얄람어 Manage Workspaces 작업 영역 관리 Manually 수동 Mark Older Files as [Converted] When Opened 열 때 이전 파일을 [Converted]로 표시 + Use System Defaults for Edit Original 원본 편집에 시스템 기본값 사용 Mark as trapped 트래핑됨으로 표시 Marks 표시 Match User Interface Brightness 사용자 인터페이스 밝기에 맞춤 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index a32aa110e..ce5749745 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -3342,6 +3342,7 @@ theme Appearance Vormgeving List Preferences Voorkeuren weergeven Manually Handmatig Mark Older Files as [Converted] When Opened Oudere bestanden bij het openen markeren als [geconverteerd] + Use System Defaults for Edit Original Systeemstandaarden gebruiken voor Origineel bewerken Match User Interface Brightness Aanpassen aan helderheid van gebruikersinterface Light Gray Lichtgrijs Medium Gray Middelgrijs diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 0ca41d1c3..2fe05541c 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3464,6 +3464,7 @@ theme Appearance Wygląd List Preferences Wyświetl preferencje Manually Ręcznie Mark Older Files as [Converted] When Opened Oznaczaj starsze pliki jako [Przekonwertowane] przy otwieraniu + Use System Defaults for Edit Original Używaj ustawień domyślnych systemu dla polecenia Edytuj oryginał Match User Interface Brightness Dopasuj do jasności interfejsu Light Gray Jasnoszary Medium Gray Średnio szary diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 2639bf40e..8eefed3c3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1585,6 +1585,7 @@ Manage Workspaces… Gerenciar Espaços de Trabalho… Manually Manualmente Mark Older Files as [Converted] When Opened Marcar Arquivos Antigos como [Convertido] Ao Abrir + Use System Defaults for Edit Original Usar Padrões do Sistema para Editar Original Mark as trapped Marcar como sobreimpresso Marker… Marcador… Marks Marcas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 5b00ad23f..b8151defb 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1890,6 +1890,7 @@ file-filter Templates Шаблоны Manually Вручную Mark as trapped Пометить как треппинг Mark Older Files as [Converted] When Opened Помечать старые файлы как [Преобразованные] при открытии + Use System Defaults for Edit Original Использовать системные значения по умолчанию для «Редактировать оригинал» Marker… Маркер… Marks Метки Marks and Bleed Метки и вылет diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 49f70795a..b0f8e46ee 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1805,6 +1805,7 @@ font-technology Variable Variabla Manually Manuellt Mark as trapped Märk som överfyllt Mark Older Files as [Converted] When Opened Märk äldre filer som [Konverterad] när de öppnas + Use System Defaults for Edit Original Använd systemets standardinställningar för Redigera original Marker… Markör… Marks Märken Marks and Bleed Märken och utfall diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 9e9a07513..b101f8bda 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1699,6 +1699,7 @@ font-technology Variable Değişken Manually Manuel olarak Mark as trapped Sıkışmış olarak işaretle Mark Older Files as [Converted] When Opened Eski Dosyaları Açıldığında [Dönüştürülmüş] Olarak İşaretle + Use System Defaults for Edit Original Orijinali Düzenle için Sistem Varsayılanlarını Kullan Marker… işaretleyici… Marks İşaretler Marks and Bleed İşaretler ve Taşma Payı diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index cc19692ed..648507919 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1887,6 +1887,7 @@ file-filter Templates Шаблони Manually Вручну Mark as trapped Позначити як трепінг Mark Older Files as [Converted] When Opened Помічати старі файли як [Перетворені] при відкритті + Use System Defaults for Edit Original Використовувати системні типові програми для «Редагувати оригінал» Marker… Маркер… Marks Мітки Marks and Bleed Мітки та виліт diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index cb106b4ef..e7f0c795f 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2172,6 +2172,7 @@ digits Hindi Hindi Manually Thủ công Mark as trapped Đánh dấu đã chồng màu Mark Older Files as [Converted] When Opened Đánh dấu tệp cũ là [Đã chuyển đổi] khi mở + Use System Defaults for Edit Original Dùng mặc định hệ thống cho Chỉnh sửa bản gốc Marker… Ký hiệu đánh dấu… Marks Dấu in Marks and Bleed Dấu in và chảy tràn diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 234d02339..5182ce479 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1774,6 +1774,7 @@ file-filter Templates 模板 Manually 手动 Mark as trapped 标示为已陷印 Mark Older Files as [Converted] When Opened 打开旧文件时标记为 [已转换] + Use System Defaults for Edit Original “编辑原稿”使用系统默认设置 Marker… 标记… Marks 标记 Marks and Bleed 标记和出血 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 21f1b12af..06f5f40f5 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2728,6 +2728,7 @@ file-filter Templates 範本 Manually 手動 Mark as trapped 標示為已陷印 Mark Older Files as [Converted] When Opened 開啟舊檔時標示為 [已轉換] + Use System Defaults for Edit Original 「編輯原稿」使用系統預設值 Marker… 標記… Marks 標記 Marks and Bleed 標記與出血 diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index 89c102f35..38a67400a 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -18,7 +18,9 @@ mod open; mod save; pub use export::{EXPORT_CANCELLED, cancel_exports, export, export_for_screens, export_pdf, hide_exports, reveal, save_command_output}; -pub(crate) use export::{export_files, exported_with_notes, file_url, open_in_app, reveal_path, run_to_file, save_command_output_named}; +pub(crate) use export::{ + export_files, exported_with_notes, file_url, open_in_app, open_original, reveal_path, run_to_file, save_command_output_named, +}; pub(crate) use open::{EXPORT_PRESETS, import_presets, opened}; pub use open::{ MAX_RECENT_FILES, OpensAs, ask_revert, install_module, install_plugin, new_from_template, note_recent, open_bytes, open_dialog, open_document, diff --git a/crates/ui-egui/src/io/export.rs b/crates/ui-egui/src/io/export.rs index 60c7656dd..f32323151 100644 --- a/crates/ui-egui/src/io/export.rs +++ b/crates/ui-egui/src/io/export.rs @@ -217,6 +217,22 @@ pub(crate) fn open_in_app(app: &mut VectorcraftApp, path: &str) -> Result<(), St app.services.open_file.as_mut().ok_or("no app to open files here")?(path) } +/// Edit Original: open `path` in the system's default app for it, or with Preferences › File +/// Handling › Use System Defaults for Edit Original off, in the app that made it (#5236): PhotoCraft +/// for a raster image where it is installed. Any other file, or no PhotoCraft, uses the system's app. +pub(crate) fn open_original(app: &mut VectorcraftApp, path: &str) -> Result<(), String> { + let ext = std::path::Path::new(path).extension().map(|e| e.to_string_lossy().into_owned()).unwrap_or_default(); + let raster = vectorcraft_engine::cmd::fileio::readable_format(&ext).is_some_and(|f| f.raster); + if !app.session.prefs.edit_original_system_defaults + && raster + && let Some(open) = app.services.open_in_photocraft.as_mut() + && open(path)? + { + return Ok(()); + } + open_in_app(app, path) +} + /// File → Export for Screens with `document.exportForScreens` params: the files go into `folder`; /// without one the web downloads them (the ZIP, or each file) and the other frontends get them /// back as base64. With `openLocation`, the file manager shows the first file written. When an diff --git a/crates/ui-egui/src/panels/links.rs b/crates/ui-egui/src/panels/links.rs index 9e751cf1f..9fc91f506 100644 --- a/crates/ui-egui/src/panels/links.rs +++ b/crates/ui-egui/src/panels/links.rs @@ -227,7 +227,7 @@ pub(crate) fn open_file(app: &mut VectorcraftApp, p: &Value, reveal: bool) -> Re } else { app.refuse_automation("links.editOriginal")?; openable_original(&path)?; - crate::io::open_in_app(app, &path)?; + crate::io::open_original(app, &path)?; } Ok(json!({ "path": path })) } diff --git a/crates/ui-egui/src/panels/tests_links.rs b/crates/ui-egui/src/panels/tests_links.rs index 98e1dd250..215cf7148 100644 --- a/crates/ui-egui/src/panels/tests_links.rs +++ b/crates/ui-egui/src/panels/tests_links.rs @@ -1,7 +1,7 @@ //! The Links panel, its flyout, Link Info, Edit Original / Show in Folder, the Placement Options //! dialog and the Properties panel's image section, drawn headless. -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::rc::Rc; use serde_json::json; @@ -168,6 +168,44 @@ fn edit_original_and_show_in_folder_hand_the_file_to_the_system() { assert!(app.run("links.editOriginal", json!({"id": linked})).is_err()); } +/// #5236: with Preferences › File Handling › Use System Defaults for Edit Original off, Edit +/// Original opens a raster image in the app that made it, PhotoCraft, and falls back to the +/// system's default app when PhotoCraft isn't installed or the file isn't a raster image. +#[test] +fn edit_original_opens_raster_images_in_photocraft_without_system_defaults() { + /// What each open asked for: (`true` for PhotoCraft, the path). + type Opened = Rc>>; + let dir = Folder::new("with"); + let (mut app, linked, _) = app(&dir); + let opened: Opened = Rc::default(); + let installed = Rc::new(Cell::new(true)); + let (o, w, i) = (opened.clone(), opened.clone(), installed.clone()); + app.services.open_file = Some(Box::new(move |p: &str| { + o.borrow_mut().push((false, p.to_string())); + Ok(()) + })); + app.services.open_in_photocraft = Some(Box::new(move |p: &str| { + if i.get() { + w.borrow_mut().push((true, p.to_string())); + } + Ok(i.get()) + })); + // On (the default): the system's app. + let path = app.run("links.editOriginal", json!({"id": linked})).unwrap()["path"].as_str().unwrap().to_string(); + app.run("prefs.set", json!({"key": "editOriginalSystemDefaults", "value": false})).unwrap(); + app.run("links.editOriginal", json!({"id": linked})).unwrap(); + installed.set(false); + app.run("links.editOriginal", json!({"id": linked})).unwrap(); + installed.set(true); + // A file that isn't a raster image goes to the system's app. + crate::io::open_original(&mut app, "/Art/plan.emf").unwrap(); + assert_eq!(*opened.borrow(), [(false, path.clone()), (true, path.clone()), (false, path), (false, "/Art/plan.emf".to_string())]); + // PhotoCraft failing to open it is said, not hidden behind the system's app. + app.services.open_in_photocraft = Some(Box::new(|_: &str| Err("PhotoCraft didn't start".to_string()))); + assert!(app.run("links.editOriginal", json!({"id": linked})).is_err()); + assert_eq!(opened.borrow().len(), 4); +} + /// Edit Original hands only regular image and document files to the system's default app. #[test] fn edit_original_opens_image_and_document_files_only() { diff --git a/crates/ui-egui/src/prefs_dialog/tests.rs b/crates/ui-egui/src/prefs_dialog/tests.rs index 80fb23a17..2e4711505 100644 --- a/crates/ui-egui/src/prefs_dialog/tests.rs +++ b/crates/ui-egui/src/prefs_dialog/tests.rs @@ -391,3 +391,18 @@ fn hyphenation_exceptions_dialog_ok_reaches_the_hyphenator() { assert!(vectorcraft_text::hyphen::hyphen_points("typography").is_empty()); assert_eq!(vectorcraft_text::hyphen::hyphen_points("happen"), vec![3]); } + +/// #5236: File Handling › Use System Defaults for Edit Original is a checkbox, on by default, and +/// OK saves it off. +#[test] +fn use_system_defaults_for_edit_original_is_a_checkbox() { + let mut a = VectorcraftApp::new(Session::new(), Default::default()); + assert!(a.session.prefs.edit_original_system_defaults, "on by default"); + open(&mut a, Some("File Handling")); + let text = crate::tests_labels::painted_text(&mut a, |a, ui| show(a, ui.ctx())); + assert!(text.contains("Use System Defaults for Edit Original"), "{text}"); + assert!(a.ui.dialog.as_ref().unwrap().bool("editOriginalSystemDefaults")); + a.ui.dialog.as_mut().unwrap().fields.insert("editOriginalSystemDefaults".into(), json!(false)); + confirm(&mut a).unwrap(); + assert!(!a.session.prefs.edit_original_system_defaults); +} diff --git a/crates/ui-egui/src/services.rs b/crates/ui-egui/src/services.rs index 45f363eda..3d3524788 100644 --- a/crates/ui-egui/src/services.rs +++ b/crates/ui-egui/src/services.rs @@ -65,6 +65,9 @@ pub(crate) fn browsable_url(url: &str) -> Result<(), String> { /// Shows a file in the system file manager, or opens it in its app. pub type RevealFn = Box Result<(), String>>; +/// Opens a file in a fixed app; `Ok(false)` when that app isn't installed. +pub type OpenInFn = Box Result>; + /// Platform services injected by the host app (desktop or web). #[derive(Default)] pub struct Services { @@ -116,6 +119,10 @@ pub struct Services { /// Open a file (or a folder) in the system's default app for it (desktop: Edit Original, Show /// Package). Without it (the web) those answer with an error. pub open_file: Option, + /// Open a file in PhotoCraft where it is installed (desktop: Edit Original of a raster image + /// with Preferences › File Handling › Use System Defaults for Edit Original off); `Ok(false)` + /// when it isn't. Without it Edit Original uses `open_file`. + pub open_in_photocraft: Option, /// Show a folder picker; returns its path (desktop: Relink to Folder, Package). pub pick_folder: Option Option>>, /// Show the system's color sampler, which picks a color anywhere on the screen (macOS, and diff --git a/crates/ui-egui/src/tests/font_menu.rs b/crates/ui-egui/src/tests/font_menu.rs index 624c32c7e..fa80c359d 100644 --- a/crates/ui-egui/src/tests/font_menu.rs +++ b/crates/ui-egui/src/tests/font_menu.rs @@ -206,6 +206,9 @@ fn a_press_outside_the_menu_ends_the_preview_at_once() { /// A document with point type in Inter, selected; its id. fn selected_inter(app: &mut VectorcraftApp) -> u64 { app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + // These tests exercise font picking, including system fonts without Latin glyphs. Missing + // glyph protection would intentionally keep the sample text in its previous font. + app.run("prefs.set", json!({"key": "missingGlyphProtection", "value": false})).unwrap(); let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Gagaku", "font": "Inter"})).unwrap()["id"].as_u64().unwrap(); app.session.execute("select.set", &json!({"ids": [id]})).unwrap(); if !crate::panel_docking::showing(&app.ui, "character") { diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 3da08ad0f..cdb7471df 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -668,6 +668,11 @@ one undo step. The Links panel (`window.panel {panel: "links"}`) shows `links.li `links.info`; `links.editOriginal` and `links.reveal` (Edit › Edit Original, Show in Folder) hand the linked file to the system and return `{path}`. Edit Original opens only a regular file whose extension is a format `document.open` reads, and answers a control request with an error unless the file is a placed document (which opens in a tab). +The preference `editOriginalSystemDefaults` (Preferences › File Handling › Use System Defaults for Edit Original, on +by default, #5236) opens the file in the system's default app for it; off, in the app that made it: a raster image in +PhotoCraft, looked for only where it installs (the Applications folders on macOS; on Windows the path its installer +registers under App Paths, then `ArtCraft\PhotoCraft` in the per-user and per-machine program folders, then the `PATH`; +the `PATH` elsewhere), and any other file, or a raster image without PhotoCraft installed, in the system's default app. Package: File → Package… (`ui.packageDialog`) opens the `package` dialog for a saved document (a document never saved gets a `confirm` dialog whose OK runs `file.saveAs`). Fields: `folder` (the location), `name` (the package folder), diff --git a/docs/mcp.md b/docs/mcp.md index 6878e137b..bbd4451f5 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4216,7 +4216,9 @@ file is missing stays linked: relink it first); `links.unembed {id, path}` write clip?}` decides how a relinked or updated file takes an image's place (default `bounds`: stretched into the old bounds; `clip` puts it in a clip group of the old bounds when it is larger). The UI commands `links.editOriginal` and `links.reveal` open the linked file in its app or show it in its folder (desktop). Edit Original runs only from the app's window, -except for a placed document, which opens in a tab. +except for a placed document, which opens in a tab. With the preference `editOriginalSystemDefaults` (Preferences › +File Handling › Use System Defaults for Edit Original, on by default) off, Edit Original opens a raster image in the app +that made it, PhotoCraft, where it is installed, and anything else in the system's default app (#5236). ```json {"name":"run_command","arguments":{"command":"links.list","params":{"show":"missing"}}} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 497b507b5..ece0bc408 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (independent Help Bar visibility; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (independent Help Bar visibility; font-family fields accept the mouse wheel, #5253; Units › East Asian Type acts, with Q and Ha units; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence