mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 06:31:19 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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![],
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user