Merge commit 'refs/check/mac2-help-bar-main-current' into feat/help-bar-visibility

# Conflicts:
#	docs/target-app-parity.md
This commit is contained in:
dotCooCoo
2026-11-03 05:09:58 -08:00
39 changed files with 406 additions and 5 deletions
+280
View File
@@ -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<bool, String> {
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<std::path::PathBuf> {
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<std::path::PathBuf> {
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::path::PathBuf> = [
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<std::path::PathBuf>,
program_folders: &[std::path::PathBuf],
on_path: Vec<std::path::PathBuf>,
) -> Vec<std::path::PathBuf> {
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<std::path::PathBuf> {
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<std::path::PathBuf> {
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<std::path::PathBuf> {
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);
}
}
+2
View File
@@ -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),
+5
View File
@@ -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 "<name> [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![],
+1
View File
@@ -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 Маркери и изтичане
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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á
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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å
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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 Σημάδια και ξάκρισμα
Can't render this file because it contains an unexpected character in line 163 and column 5.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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ó
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2211 and column 5.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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 ユーザーインターフェイスの明るさに合わせる
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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 საშუალო ნაცრისფერი
Can't render this file because it is too large.
+1
View File
@@ -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 사용자 인터페이스 밝기에 맞춤
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -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 Метки и вылет
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -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ı
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -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 Мітки та виліт
Can't render this file because it contains an unexpected character in line 177 and column 5.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 199 and column 5.
+1
View File
@@ -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 标记和出血
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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 標記與出血
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -1
View File
@@ -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,
+16
View File
@@ -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
+1 -1
View File
@@ -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 }))
}
+39 -1
View File
@@ -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<RefCell<Vec<(bool, String)>>>;
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() {
+15
View File
@@ -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);
}
+7
View File
@@ -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<dyn FnMut(&str) -> Result<(), String>>;
/// Opens a file in a fixed app; `Ok(false)` when that app isn't installed.
pub type OpenInFn = Box<dyn FnMut(&str) -> Result<bool, String>>;
/// 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<RevealFn>,
/// 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<OpenInFn>,
/// Show a folder picker; returns its path (desktop: Relink to Folder, Package).
pub pick_folder: Option<Box<dyn FnMut() -> Option<String>>>,
/// Show the system's color sampler, which picks a color anywhere on the screen (macOS, and
+3
View File
@@ -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") {
+5
View File
@@ -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),
+3 -1
View File
@@ -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"}}}
+1 -1
View File
@@ -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