Fix #5236: Edit Original is one checkbox, Use System Defaults for Edit Original

Per review on #5327: Preferences › File Handling › Edit Original With (an app
path and Choose…) becomes the checkbox Use System Defaults for Edit Original,
on by default. Off, Edit Original opens a raster image in the app that made
it, PhotoCraft, looked for only where it installs; any other file, or no
PhotoCraft, goes to the system's default app. The preference is a boolean, so
prefs.set over control or MCP can no longer name an executable. The label has
a row in all 26 catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ruchirguitar
2026-11-03 19:57:04 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 79c8d1a7e0
commit d7aa1c561f
36 changed files with 113 additions and 226 deletions
+22 -30
View File
@@ -300,8 +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 with Preferences › File Handling › Edit Original With set (#5236).
open_file_with: Some(Box::new(open_file_with)),
// 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")]
@@ -337,14 +337,14 @@ fn open_file(path: &str) -> Result<(), String> {
c.spawn().map(|_| ()).map_err(|e| format!("can't open {path}: {e}"))
}
/// Edit Original with Preferences › File Handling › Edit Original With set (#5236): open `path` in
/// `app` (`photocraft`, or an application's path). The app must be an application ([`check_app`]);
/// it gets the path as its only argument and is never started through a shell.
fn open_file_with(app: &str, path: &str) -> Result<(), String> {
/// 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 app = resolve_app(app, home.as_deref(), std::env::var_os("PATH").as_deref())?;
check_app(&app)?;
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");
@@ -369,7 +369,7 @@ fn open_file_with(app: &str, path: &str) -> Result<(), String> {
let started = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) if status.success() => return Ok(()),
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() {
@@ -385,26 +385,19 @@ fn open_file_with(app: &str, path: &str) -> Result<(), String> {
std::thread::spawn(move || {
let _ = child.wait();
});
return Ok(());
return Ok(true);
}
Err(e) => return Err(fail(e.to_string())),
}
}
}
#[cfg(not(target_os = "macos"))]
c.spawn().map(|_| ()).map_err(|e| format!("can't open {path} in {}: {e}", app.display()))
c.spawn().map(|_| true).map_err(|e| format!("can't open {path} in {}: {e}", app.display()))
}
/// The application Edit Original With names: PhotoCraft where it is installed for `photocraft`
/// ([`photocraft_candidates`]), else the path as given.
pub(crate) fn resolve_app(app: &str, home: Option<&std::path::Path>, path_var: Option<&std::ffi::OsStr>) -> Result<std::path::PathBuf, String> {
if app.trim().eq_ignore_ascii_case("photocraft") {
return photocraft_candidates(home, path_var)
.into_iter()
.find(|p| check_app(p).is_ok())
.ok_or_else(|| "PhotoCraft isn't installed: choose another app in Preferences › File Handling › Edit Original With".to_string());
}
Ok(std::path::PathBuf::from(app.trim()))
/// 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, else a `photocraft`
@@ -421,7 +414,7 @@ pub(crate) fn photocraft_candidates(home: Option<&std::path::Path>, path_var: Op
/// 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) is refused, so the preference can only ever open an installed app.
/// 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") {
@@ -447,7 +440,7 @@ fn executable_file(p: &std::path::Path) -> bool {
#[cfg(test)]
mod open_with_tests {
use super::{check_app, photocraft_candidates, resolve_app};
use super::{check_app, find_photocraft, photocraft_candidates};
/// A fresh, empty folder under the system's temporary folder.
fn scratch(name: &str) -> std::path::PathBuf {
@@ -457,8 +450,8 @@ mod open_with_tests {
d
}
/// #5236: Edit Original With only ever starts an application, never a document, a folder or a
/// missing path.
/// #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");
@@ -484,22 +477,21 @@ mod open_with_tests {
let _ = std::fs::remove_dir_all(&d);
}
/// `photocraft` is looked for where PhotoCraft installs; any other value is taken as a path.
/// PhotoCraft is looked for only where it installs.
#[test]
fn photocraft_is_found_where_it_installs() {
let d = scratch("find");
assert_eq!(resolve_app("/Apps/Other.app", None, None).unwrap(), std::path::PathBuf::from("/Apps/Other.app"));
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!(resolve_app("PhotoCraft", Some(&d), None).unwrap().ends_with("PhotoCraft.app"));
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();
assert!(resolve_app("photocraft", None, Some(&path_var)).is_err(), "not installed");
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)]
@@ -507,7 +499,7 @@ mod open_with_tests {
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755)).unwrap();
}
assert_eq!(resolve_app("photocraft", None, Some(&path_var)).unwrap(), exe);
assert_eq!(find_photocraft(None, Some(&path_var)), Some(exe));
}
let _ = std::fs::remove_dir_all(&d);
}
+2 -2
View File
@@ -372,8 +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),
// "" (the system's default app), `photocraft` or an application's path (#5236).
p!("editOriginalApp", "File Handling", "Files", "Edit Original With", 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 -5
View File
@@ -253,10 +253,10 @@ pub struct Prefs {
// File Handling (continued)
/// Where Save as Template and New from Template start ("" = `Documents/VectorCraft Templates`).
pub templates_folder: String,
/// The app Links panel › Edit Original opens a linked file in (#5236): "" = the system's
/// default app for the file, `photocraft` = PhotoCraft wherever it is installed, else the path
/// of an application.
pub edit_original_app: 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
@@ -468,7 +468,7 @@ impl Default for Prefs {
pdf_presets: vec![],
screen_export_presets: vec![],
templates_folder: String::new(),
edit_original_app: 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.
+12 -6
View File
@@ -217,14 +217,20 @@ 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 app Preferences › File Handling › Edit Original With names
/// (#5236), or in the system's default app for it when that is System Default ("").
/// 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 with = app.session.prefs.edit_original_app.trim().to_string();
if with.is_empty() {
return open_in_app(app, path);
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(());
}
app.services.open_file_with.as_mut().ok_or("no app to open files in here")?(&with, path)
open_in_app(app, path)
}
/// File → Export for Screens with `document.exportForScreens` params: the files go into `folder`;
+25 -29
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,44 +168,40 @@ fn edit_original_and_show_in_folder_hand_the_file_to_the_system() {
assert!(app.run("links.editOriginal", json!({"id": linked})).is_err());
}
/// #5236: Preferences › File Handling › Edit Original With picks the app Edit Original opens the
/// file in: System Default ("") hands it to the system's default app as before, PhotoCraft or an
/// application's path to that app.
/// #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_the_file_in_the_chosen_app() {
/// What each open asked for: (the app, `None` for the system's default, and the path).
type Opened = Rc<RefCell<Vec<(Option<String>, String)>>>;
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 (o, w) = (opened.clone(), opened.clone());
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((None, p.to_string()));
o.borrow_mut().push((false, p.to_string()));
Ok(())
}));
app.services.open_file_with = Some(Box::new(move |with: &str, p: &str| {
w.borrow_mut().push((Some(with.to_string()), 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();
for value in ["photocraft", "/Applications/Example.app"] {
app.run("prefs.set", json!({"key": "editOriginalApp", "value": value})).unwrap();
app.run("links.editOriginal", json!({"id": linked})).unwrap();
}
app.run("prefs.set", json!({"key": "editOriginalApp", "value": ""})).unwrap();
app.run("prefs.set", json!({"key": "editOriginalSystemDefaults", "value": false})).unwrap();
app.run("links.editOriginal", json!({"id": linked})).unwrap();
assert_eq!(
*opened.borrow(),
[
(None, path.clone()),
(Some("photocraft".into()), path.clone()),
(Some("/Applications/Example.app".into()), path.clone()),
(None, path.clone()),
]
);
// An app the host can't open in (the web) is an error, not the default app instead.
app.run("prefs.set", json!({"key": "editOriginalApp", "value": "photocraft"})).unwrap();
app.services.open_file_with = None;
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);
}
-94
View File
@@ -176,7 +176,6 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
let Some(mut d) = app.ui.dialog.clone() else { return };
let t = Tokens::get(ctx);
let (mut ok, mut cancel, mut reset) = (false, false, false);
d.fields.insert(CAN_PICK_APP.into(), json!(crate::picks::can(app, &crate::picks::PickRequest::Open(app_pick()))));
let cat = d.str("__category");
let cat = PREF_CATEGORIES.iter().find(|c| **c == cat).copied().unwrap_or(PREF_CATEGORIES[0]);
crate::dialogs::modal::show(ctx, tl!("Preferences"), egui::Id::unique("dialog-preferences"), -20.0, 20, |ui| {
@@ -242,10 +241,6 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
for (k, v) in Prefs::default().to_json().as_object().cloned().unwrap_or_default() {
d.fields.insert(k, v);
}
d.fields.remove(CHOSE_OTHER_APP);
}
if d.fields.remove(CHOOSE_APP).is_some() {
choose_app(app, &mut d);
}
app.ui.dialog = Some(d);
if cancel {
@@ -385,7 +380,6 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) {
}
});
}
PrefKind::Text if sp.key == "editOriginalApp" => edit_original_row(ui, d, sp.key, sp.label, &v),
PrefKind::Text => {
labeled(ui, sp.label, |ui| {
let mut s = v.as_str().unwrap_or("").to_string();
@@ -416,94 +410,6 @@ fn choice_row(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, opts: &
});
}
/// File Handling › Edit Original With (#5236): System Default (""), PhotoCraft (`photocraft`) or
/// Other Application, which shows a field for the application's path.
fn edit_original_row(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, v: &Value) {
const OPTIONS: [&str; 3] = ["System Default", "PhotoCraft", "Other Application"];
// The field shows the value as stored, spaces included (the host trims it when it opens).
let raw = v.as_str().unwrap_or("").to_string();
let cur = raw.trim().to_string();
let photocraft = cur.eq_ignore_ascii_case("photocraft");
// Other Application with no path typed yet is still System Default: the dialog remembers the
// choice in a field of its own, which a new dialog and Reset Preferences start without.
let mut other = !photocraft && (!cur.is_empty() || d.bool(CHOSE_OTHER_APP));
// A path (saved or typed) also counts as the choice, so clearing it to type another keeps the
// field.
if other && !d.bool(CHOSE_OTHER_APP) {
d.fields.insert(CHOSE_OTHER_APP.into(), json!(true));
}
let at = if photocraft {
1
} else if other {
2
} else {
0
};
labeled(ui, label, |ui| {
if let Some(i) = widgets::dropdown(ui, key, OPTIONS[at], &OPTIONS, 260.0) {
other = i == 2;
d.fields.insert(CHOSE_OTHER_APP.into(), json!(other));
let new = match i {
0 => String::new(),
1 => "photocraft".to_string(),
_ if photocraft => String::new(),
_ => raw.clone(),
};
d.fields.insert(key.into(), json!(new));
}
});
if other {
let can_pick = d.bool(CAN_PICK_APP);
labeled(ui, "Application Path", |ui| {
let mut s = if photocraft { String::new() } else { raw.clone() };
let width = if can_pick { 186.0 } else { 260.0 };
let field = egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(width);
let hint = if cfg!(target_os = "macos") { "/Applications/Example.app" } else { "" };
if ui.add(field.hint_text(hint)).changed() {
d.fields.insert(key.into(), json!(s));
}
// The open dialog runs from `show`, which has the app ([`choose_app`]).
if can_pick && widgets::flat_button(ui, tl!("Choose…"), 68.0).clicked() {
d.fields.insert(CHOOSE_APP.into(), json!(true));
}
});
}
}
/// UI-only dialog fields for Edit Original With's Choose… button: whether the host shows an open
/// dialog, and that the button was clicked this frame.
const CAN_PICK_APP: &str = "__canPickApp";
const CHOOSE_APP: &str = "__chooseApp";
/// UI-only dialog field: Other Application was chosen, though no path is typed yet.
const CHOSE_OTHER_APP: &str = "__choseOtherApp";
/// The open dialog Edit Original With's Choose… shows: applications (`.app` bundles in
/// Applications on macOS, `.exe` files on Windows, any file elsewhere).
fn app_pick() -> crate::FilePick {
let filters: Vec<crate::FileFilter> = if cfg!(target_os = "macos") {
vec![("Applications".into(), vec!["app".into()])]
} else if cfg!(windows) {
vec![("Programs".into(), vec!["exe".into()])]
} else {
vec![]
};
let folder = cfg!(target_os = "macos").then(|| "/Applications".to_string());
crate::FilePick { name: String::new(), folder, filters }
}
/// Edit Original With › Choose…: the picked application into the dialog's `editOriginalApp`. Shown
/// off the UI thread, the answer fills the dialog as it is then ([`crate::picks::in_dialog`]).
fn choose_app(app: &mut VectorcraftApp, d: &mut Dialog) {
let r = crate::picks::in_dialog(app, d, |app, d| {
if let Some(path) = crate::picks::open(app, &app_pick()) {
d.fields.insert("editOriginalApp".into(), json!(path));
}
Ok(())
});
// Picking a file doesn't fail.
let _ = r;
}
fn bool_row(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) {
// The app's outlined checkbox (it translates `label`): egui's own has no border at rest.
let on = d.bool(key);
+8 -47
View File
@@ -392,56 +392,17 @@ fn hyphenation_exceptions_dialog_ok_reaches_the_hyphenator() {
assert_eq!(vectorcraft_text::hyphen::hyphen_points("happen"), vec![3]);
}
/// #5236: File Handling › Edit Original With offers System Default, PhotoCraft and Other
/// Application, whose Choose… puts the picked application into the preference.
/// #5236: File Handling › Use System Defaults for Edit Original is a checkbox, on by default, and
/// OK saves it off.
#[test]
fn edit_original_with_takes_a_chosen_application() {
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("Edit Original With") && text.contains("System Default"), "{text}");
let asked: std::rc::Rc<std::cell::RefCell<Vec<crate::FilePick>>> = Default::default();
let seen = asked.clone();
a.services.pick_open = Some(Box::new(move |p: &crate::FilePick| {
seen.borrow_mut().push(p.clone());
Some("/Applications/Editor.app".into())
}));
let mut d = a.ui.dialog.clone().unwrap();
choose_app(&mut a, &mut d);
assert_eq!(d.str("editOriginalApp"), "/Applications/Editor.app");
let pick = asked.borrow()[0].clone();
if cfg!(target_os = "macos") {
assert_eq!((pick.folder.as_deref(), pick.filters[0].1.clone()), (Some("/Applications"), vec!["app".to_string()]));
}
// OK saves it; the dialog's own fields (`__…`) are not preferences.
a.ui.dialog = Some(d);
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_eq!(a.session.prefs.edit_original_app, "/Applications/Editor.app");
}
/// #5327 review: choosing Other Application before a path is typed is remembered by the dialog
/// itself, so a dialog opened again (or reset) starts at System Default, and a stored path keeps
/// its spaces.
#[test]
fn edit_original_with_other_is_the_dialogs_own_choice() {
let mut a = VectorcraftApp::new(Session::new(), Default::default());
open(&mut a, Some("File Handling"));
assert!(!a.ui.dialog.as_ref().unwrap().bool(CHOSE_OTHER_APP), "a new dialog starts without the choice");
let shown = |a: &mut VectorcraftApp| crate::tests_labels::painted_text(a, |a, ui| show(a, ui.ctx())).contains("Application Path");
assert!(!shown(&mut a), "System Default shows no path field");
a.ui.dialog.as_mut().unwrap().fields.insert(CHOSE_OTHER_APP.into(), json!(true));
assert!(shown(&mut a), "Other Application with no path yet shows the field");
a.ui.dialog = None;
open(&mut a, Some("File Handling"));
assert!(!shown(&mut a), "opened again: System Default");
// A saved path counts as the choice: clearing it to type another keeps the field.
a.ui.dialog.as_mut().unwrap().fields.insert("editOriginalApp".into(), json!("/Applications/Old.app"));
assert!(shown(&mut a));
a.ui.dialog.as_mut().unwrap().fields.insert("editOriginalApp".into(), json!(""));
assert!(shown(&mut a), "the path cleared: still Other Application");
// A path with spaces is kept as typed.
let path = "/Applications/Visual Studio Code.app";
a.ui.dialog.as_mut().unwrap().fields.insert("editOriginalApp".into(), json!(path));
confirm(&mut a).unwrap();
assert_eq!(a.session.prefs.edit_original_app, path);
assert!(!a.session.prefs.edit_original_system_defaults);
}
+6 -6
View File
@@ -65,8 +65,8 @@ 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 chosen app: `(app, path)`.
pub type OpenWithFn = Box<dyn FnMut(&str, &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)]
@@ -119,10 +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 a chosen app (desktop: Edit Original with Preferences › File Handling › Edit
/// Original With set): `(app, path)`, where `app` is `photocraft` or an application's path.
/// Without it (the web) Edit Original with an app set answers with an error.
pub open_file_with: Option<OpenWithFn>,
/// 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
+4 -4
View File
@@ -668,10 +668,10 @@ 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 `editOriginalApp` (Preferences › File Handling › Edit Original With, #5236) picks the app: `""` (System
Default, the system's default app for the file), `"photocraft"` (PhotoCraft where it is installed) or an application's
path. The app must be an application (an `.app` bundle on macOS, an `.exe` on Windows, an executable file elsewhere),
and gets the file's path as its only argument.
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, 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 -3
View File
@@ -4216,9 +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. The preference `editOriginalApp` (Preferences › File Handling ›
Edit Original With) picks the app Edit Original opens the file in: `""` (the system's default app), `"photocraft"` or an
application's path (#5236).
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"}}}