collective

Browse / github.com/storytold/photocraft / patch

feat(ui-egui): File Handling preference Save As to Original Folder (#9656)

[Feature Request] Save As should default to the open document's folder ### Summary When I open a project file from one folder and then use **File → Save As…**, PhotoCraft opens the Save As dialog in the folder where I last saved a file, not in the folder containing the currently open document. I would like PhotoCraft to offer an option (or make this the default behavior) so that **Save As…** starts in the same directory as the currently open project file. ### Current behavior 1. Open a `.pcraft`, PSD, or other supported project file from, for example, `D:\Projects\ClientA`. 2. Use **File → Save As…**. 3. The dialog opens in the last-used save location, for ex

Status
open
Score
0.00
Adopters
0 other operators
Author
SofiaAFigueredo (imported from pull request #9662, open)
Base
7ac7b8b106acccf6826d45ed5c9e481dfe341035
Size
+141 −6 in 27 files
Review
pending
Shared
2026-10-28
Id
29b324127812f8f4ebe0cc72

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/prefs.rs crates/ui-egui/src/app/files.rs crates/ui-egui/src/file_dialog.rs crates/ui-egui/src/file_dialog/tests.rs crates/ui-egui/src/i18n/cs.tsv crates/ui-egui/src/i18n/de.tsv crates/ui-egui/src/i18n/el.tsv crates/ui-egui/src/i18n/es.tsv crates/ui-egui/src/i18n/fr.tsv crates/ui-egui/src/i18n/hu.tsv crates/ui-egui/src/i18n/id.tsv crates/ui-egui/src/i18n/it.tsv crates/ui-egui/src/i18n/ja.tsv crates/ui-egui/src/i18n/ko.tsv crates/ui-egui/src/i18n/nl.tsv crates/ui-egui/src/i18n/pl.tsv crates/ui-egui/src/i18n/pt-br.tsv crates/ui-egui/src/i18n/ru.tsv crates/ui-egui/src/i18n/sr.tsv crates/ui-egui/src/i18n/tr.tsv crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/src/i18n/vi.tsv crates/ui-egui/src/i18n/zh-hans.tsv crates/ui-egui/src/i18n/zh-hant.tsv crates/ui-egui/src/prefs/prefs_ui/fields.rs crates/ui-egui/src/prefs/prefs_ui/tests.rs docs/scorecard.md

diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs--- a/crates/engine/src/prefs.rs+++ b/crates/engine/src/prefs.rs@@ -473,6 +473,9 @@ impl Default for HistoryLog { pub struct FileHandling {     pub image_previews: Ask,     pub lowercase_extension: bool,+    /// Save As and the export dialogs start in the open document's folder (on, the default), or+    /// in the folder last saved or exported to across documents when off (#9656, #8493).+    pub save_as_to_original_folder: bool,     /// File › Save a Copy suggests the document's own name rather than "<name> copy" (#8080).     pub do_not_append_copy: bool,     /// Saves run in the background while you keep working (on), or in the foreground under a@@ -496,6 +499,10 @@ pub struct FileHandling {     pub recent_file_count: u32,     /// Most recently opened files, newest first (File › Open Recent).     pub recent_files: Vec<String>,+    /// The folder the last save or export dialog was answered with, for any document: where+    /// Save As and the export dialogs start with Save As to Original Folder off (#9656). Kept+    /// here so it survives a restart; the dialog does not show it.+    pub last_save_folder: String,     /// A folder whose `.cube`, `.3dl` and `.look` files Color Lookup lists under its built-in     /// looks (native only, #6558). Empty lists none.     pub lut_folder: String,@@ -514,6 +521,7 @@ impl Default for FileHandling {         Self {             image_previews: Ask::Always,             lowercase_extension: true,+            save_as_to_original_folder: true,             do_not_append_copy: false,             save_in_background: true,             autosave: true,@@ -526,6 +534,7 @@ impl Default for FileHandling {             rasterize_svg_groups_deeper_than: photocraft_doc::MAX_GROUP_DEPTH as u32,             recent_file_count: 20,             recent_files: Vec::new(),+            last_save_folder: String::new(),             lut_folder: String::new(),         }     }diff --git a/crates/ui-egui/src/app/files.rs b/crates/ui-egui/src/app/files.rs--- a/crates/ui-egui/src/app/files.rs+++ b/crates/ui-egui/src/app/files.rs@@ -199,6 +199,13 @@ impl PhotocraftApp {             }         };         let doc = st.doc.id;+        // Save As to Original Folder off: the name stays, the folder is the one last saved to.+        // File › Save of a flat file asking for a name keeps writing the document's own file.+        let own_file = plain_save && st.path.as_deref() == Some(suggested.as_str());+        let suggested = match self.last_save_dir().filter(|_| !own_file && !self.session.prefs().file_handling.save_as_to_original_folder) {+            Some(dir) => std::path::Path::new(&suggested).file_name().map_or(suggested.clone(), |name| dir.join(name).to_string_lossy().into_owned()),+            None => suggested,+        };         self.pick_save(&suggested, move |app, path| {             app.with_document(doc, |app| {                 let own = app.session.active().and_then(|st| st.path.clone());diff --git a/crates/ui-egui/src/file_dialog.rs b/crates/ui-egui/src/file_dialog.rs--- a/crates/ui-egui/src/file_dialog.rs+++ b/crates/ui-egui/src/file_dialog.rs@@ -146,17 +146,23 @@ impl PhotocraftApp {     /// A bare file name (export dialogs, an untitled document) starts in the folder the active     /// document was last saved or exported to through a dialog, else beside the document     /// (#1826, #1866). A caller's explicit directory is kept. The chosen folder is remembered for-    /// the document the dialog was asked for.+    /// the document the dialog was asked for. With Preferences ▸ File Handling › Save As to+    /// Original Folder off, a bare name starts in the folder last saved or exported to for any+    /// document instead, else beside the document (#9656).     pub(crate) fn pick_save(&mut self, suggested: &str, then: impl FnOnce(&mut Self, String) -> Result<Value, String> + 'static) -> Result<Value, String> {         let mut suggested = std::path::PathBuf::from(suggested);         let doc = self.session.active().map(|d| d.doc.id);         // Closed documents' folders are no longer needed.         let open: Vec<_> = self.session.documents().iter().map(|d| d.doc.id).collect();         self.save_dirs.retain(|id, _| open.contains(id));+        let beside_document = || self.session.active().and_then(|d| d.path.as_deref()).and_then(|p| std::path::Path::new(p).parent()).map(Path::to_path_buf);+        let remembered = if self.session.prefs().file_handling.save_as_to_original_folder {+            doc.and_then(|d| self.save_dirs.get(&d).cloned())+        } else {+            self.last_save_dir()+        };         if suggested.parent().is_some_and(|p| p.as_os_str().is_empty())-            && let Some(dir) = doc-                .and_then(|d| self.save_dirs.get(&d).cloned())-                .or_else(|| self.session.active().and_then(|d| d.path.as_deref()).and_then(|p| std::path::Path::new(p).parent()).map(Path::to_path_buf))+            && let Some(dir) = remembered.or_else(beside_document)         {             suggested = dir.join(suggested);         }@@ -174,6 +180,10 @@ impl PhotocraftApp {                 {                     app.save_dirs.insert(doc, dir.to_path_buf());                 }+                if let Some(dir) = Path::new(&path).parent().filter(|p| !p.as_os_str().is_empty()) {+                    let dir = dir.to_string_lossy().into_owned();+                    app.session.prefs.edit(|p| p.file_handling.last_save_folder = dir);+                }                 then(app, path)             }             _ => Err(UNEXPECTED.into()),@@ -184,6 +194,12 @@ impl PhotocraftApp {         Ok(asked)     } +    /// The folder the last save or export dialog was answered with, for any document, when+    /// Preferences ▸ File Handling › Save As to Original Folder is off (#9656).+    pub(crate) fn last_save_dir(&self) -> Option<std::path::PathBuf> {+        Some(self.session.prefs().file_handling.last_save_folder.as_str()).filter(|d| !d.is_empty()).map(std::path::PathBuf::from)+    }+     /// Whether a save to `path` replaces a file the save dialog didn't ask about: the dialog     /// answered `answered`, and [`saved_path`] changed it. A file at `path` that     /// [`Services::same_file`](crate::Services::same_file) finds is the same entry as `answered` (thediff --git a/crates/ui-egui/src/file_dialog/tests.rs b/crates/ui-egui/src/file_dialog/tests.rs--- a/crates/ui-egui/src/file_dialog/tests.rs+++ b/crates/ui-egui/src/file_dialog/tests.rs@@ -738,6 +738,69 @@ fn save_dialogs_remember_where_each_document_was_last_saved() {     app.poll_file_dialog(&ctx, None); } +/// #9656: Save As and the export dialogs start in the open document's folder by default; with+/// Save As to Original Folder off they start in the folder last saved to, for any document.+#[test]+fn save_as_starts_in_the_document_folder_unless_original_folder_is_off() {+    let (mut app, open, _) = app();+    let ctx = egui::Context::default();+    app.session.active_mut().unwrap().path = Some(joined("/clients/a", "design.psd"));+    let suggested = |open: &Open| match &open.borrow()[0].0 {+        FileDialogRequest::Save { suggested } => suggested.clone(),+        other => panic!("save dialog expected, got {other:?}"),+    };+    let ask = |app: &mut PhotocraftApp, open: &Open, id: &str, answer_with: Option<&str>| {+        menus::invoke(app, &ctx, id, json!({})).unwrap();+        app.poll_file_dialog(&ctx, None);+        let asked = suggested(open);+        answer(open, answer_with.map(|p| FileDialogAnswer::SaveTo(p.into())));+        app.poll_file_dialog(&ctx, None);+        asked+    };+    // The default: Save As offers the document's own path, and nothing is remembered across documents.+    assert!(app.session.prefs().file_handling.save_as_to_original_folder);+    assert_eq!(ask(&mut app, &open, "file.saveAs", Some(&joined("/exports", "design.psd"))), joined("/clients/a", "design.psd"));+    assert_eq!(app.session.prefs().file_handling.last_save_folder, Path::new("/exports").to_string_lossy());+    app.session.active_mut().unwrap().path = Some(joined("/clients/a", "design.psd"));+    assert_eq!(ask(&mut app, &open, "file.saveAs", None), joined("/clients/a", "design.psd"));+    // Off: the name stays, the folder is the one last saved to.+    app.run("prefs.set", json!({"path": "fileHandling.saveAsToOriginalFolder", "value": false})).unwrap();+    assert_eq!(ask(&mut app, &open, "file.saveAs", None), joined("/exports", "design.psd"));+    // A bare export name follows the same rule, for a document that was never saved there.+    app.pick_save("result.png", |_, _| Ok(Value::Null)).unwrap();+    app.poll_file_dialog(&ctx, None);+    assert_eq!(suggested(&open), joined("/exports", "result.png"));+    answer(&open, None);+    app.poll_file_dialog(&ctx, None);+    // Nothing remembered yet: fall back to the document's folder.+    app.run("prefs.set", json!({"path": "fileHandling.lastSaveFolder", "value": ""})).unwrap();+    assert_eq!(ask(&mut app, &open, "file.saveAs", None), joined("/clients/a", "design.psd"));+    app.pick_save("result.png", |_, _| Ok(Value::Null)).unwrap();+    app.poll_file_dialog(&ctx, None);+    assert_eq!(suggested(&open), joined("/clients/a", "result.png"));+    answer(&open, None);+    app.poll_file_dialog(&ctx, None);+}++/// #9656: with Save As to Original Folder off, File › Save of a flat file that asks for a name+/// still offers the document's own file; only Save As moves to the remembered folder.+#[test]+fn plain_save_keeps_the_documents_file_when_original_folder_is_off() {+    let ctx = egui::Context::default();+    let path = "/pics/My image.jpg";+    let (mut app, open, _) = jpeg_app(None, false);+    app.opened_from(path);+    app.run("image.adjustments.invert", json!({})).unwrap();+    app.run("prefs.set", json!({"values": {"fileHandling.saveAsToOriginalFolder": false, "fileHandling.lastSaveFolder": "/exports"}})).unwrap();+    for (id, expected) in [("file.save", path.to_string()), ("file.saveAs", joined("/exports", "My image.jpg"))] {+        menus::invoke(&mut app, &ctx, id, json!({})).unwrap();+        app.poll_file_dialog(&ctx, None);+        assert!(matches!(&open.borrow()[0].0, FileDialogRequest::Save { suggested } if *suggested == expected), "{id}");+        answer(&open, None);+        app.poll_file_dialog(&ctx, None);+    }+}+ #[test] fn save_suggestions_preserve_explicit_directories_and_untitled_names() {     let (mut app, open, _) = app();diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv--- a/crates/ui-egui/src/i18n/cs.tsv+++ b/crates/ui-egui/src/i18n/cs.tsv@@ -3911,6 +3911,7 @@ navigator	Zoom	Zvětšení 	Chisel Soft	Měkké tesání 	Open color-less EXR as ACEScg	Otevřít EXR bez údaje o barevném prostoru jako ACEScg 	Do not append "copy" to filename when saving a copy	Nepřidávat „copy“ k názvu souboru při ukládání kopie+	Save As to Original Folder	Uložit jako do původní složky  # View › Guides › right-click a guide 	Edit Selected Guides	Upravit vybraná vodítkadiff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv--- a/crates/ui-egui/src/i18n/de.tsv+++ b/crates/ui-egui/src/i18n/de.tsv@@ -3315,6 +3315,7 @@ curves	Smooth	Glätten 	Chisel Soft	Meißeln weich 	Open color-less EXR as ACEScg	Farbangabenloses EXR als ACEScg öffnen 	Do not append "copy" to filename when saving a copy	Beim Speichern einer Kopie „copy“ nicht an den Dateinamen anhängen+	Save As to Original Folder	„Speichern unter“ im Originalordner # Export As metadata and profile options 	Color Profile	Farbprofil 	EXIF	EXIFdiff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv--- a/crates/ui-egui/src/i18n/el.tsv+++ b/crates/ui-egui/src/i18n/el.tsv@@ -3997,6 +3997,7 @@ curves	Smooth	Εξομάλυνση 	Chisel Soft	Μαλακή σμίλευση 	Open color-less EXR as ACEScg	Άνοιγμα EXR χωρίς χρωματική δήλωση ως ACEScg 	Do not append "copy" to filename when saving a copy	Να μην προστίθεται «copy» στο όνομα αρχείου κατά την αποθήκευση αντιγράφου+	Save As to Original Folder	Αποθήκευση ως στον αρχικό φάκελο # Export As metadata and profile options 	EXIF	EXIF diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv--- a/crates/ui-egui/src/i18n/es.tsv+++ b/crates/ui-egui/src/i18n/es.tsv@@ -4260,6 +4260,7 @@ navigator	Zoom	Zoom 	Chisel Soft	Cincelar suave 	Open color-less EXR as ACEScg	Abrir EXR sin espacio de color como ACEScg 	Do not append "copy" to filename when saving a copy	No añadir «copy» al nombre de archivo al guardar una copia+	Save As to Original Folder	Guardar como en la carpeta original # Export As metadata and profile options  # View › Guides › right-click a guidediff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv--- a/crates/ui-egui/src/i18n/fr.tsv+++ b/crates/ui-egui/src/i18n/fr.tsv@@ -3307,6 +3307,7 @@ curves	Smooth	Lisser 	Chisel Soft	Ciselage doux 	Open color-less EXR as ACEScg	Ouvrir les EXR sans espace colorimétrique en ACEScg 	Do not append "copy" to filename when saving a copy	Ne pas ajouter « copy » au nom de fichier lors de l’enregistrement d’une copie+	Save As to Original Folder	Enregistrer sous dans le dossier d’origine  # View › Guides › right-click a guide 	Edit Selected Guides	Modifier les repères sélectionnésdiff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv--- a/crates/ui-egui/src/i18n/hu.tsv+++ b/crates/ui-egui/src/i18n/hu.tsv@@ -4190,6 +4190,7 @@ cameraRaw	Yellow	Sárga 	Chisel Soft	Lágy vésés 	Open color-less EXR as ACEScg	Színinformáció nélküli EXR megnyitása ACEScg-ként 	Do not append "copy" to filename when saving a copy	Ne fűzze a „copy” szót a fájlnévhez másolat mentésekor+	Save As to Original Folder	Mentés másként az eredeti mappába  # View › Guides › right-click a guide 	Edit Selected Guides	Kijelölt segédvonalak szerkesztésediff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv--- a/crates/ui-egui/src/i18n/id.tsv+++ b/crates/ui-egui/src/i18n/id.tsv@@ -3310,6 +3310,7 @@ curves	Smooth	Haluskan 	Chisel Soft	Pahat Lembut 	Open color-less EXR as ACEScg	Buka EXR tanpa informasi warna sebagai ACEScg 	Do not append "copy" to filename when saving a copy	Jangan tambahkan "copy" ke nama file saat menyimpan salinan+	Save As to Original Folder	Simpan Sebagai ke Folder Asli  # View › Guides › right-click a guide 	Edit Selected Guides	Edit Panduan Terpilihdiff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv--- a/crates/ui-egui/src/i18n/it.tsv+++ b/crates/ui-egui/src/i18n/it.tsv@@ -4262,6 +4262,7 @@ navigator	Zoom	Zoom 	Chisel Soft	Cesello morbido 	Open color-less EXR as ACEScg	Apri gli EXR senza spazio colore come ACEScg 	Do not append "copy" to filename when saving a copy	Non aggiungere "copy" al nome del file quando si salva una copia+	Save As to Original Folder	Salva con nome nella cartella originale  # Color management, Liquify, glyph group, warp and Camera Raw labels the Serbian, Vietnamese and Japanese catalogs added 	Adobe-rgb-compat	Adobe RGB (compatibile)diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv--- a/crates/ui-egui/src/i18n/ja.tsv+++ b/crates/ui-egui/src/i18n/ja.tsv@@ -3977,6 +3977,7 @@ curves	Smooth	スムーズ 	Chisel Soft	チゼルソフト 	Open color-less EXR as ACEScg	色空間のないEXRをACEScgとして開く 	Do not append "copy" to filename when saving a copy	コピーを保存するときにファイル名に「copy」を付けない+	Save As to Original Folder	「別名で保存」を元のフォルダーに # View › Guides › right-click a guide 	Edit Selected Guides	選択したガイドを編集 	Clear Selected Guides	選択したガイドを消去diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv--- a/crates/ui-egui/src/i18n/ko.tsv+++ b/crates/ui-egui/src/i18n/ko.tsv@@ -3502,6 +3502,7 @@ curves	Smooth	매끄럽게 	Chisel Soft	부드럽게 조각 	Open color-less EXR as ACEScg	색 공간 정보 없는 EXR을 ACEScg로 열기 	Do not append "copy" to filename when saving a copy	사본 저장 시 파일 이름에 "copy"를 붙이지 않음+	Save As to Original Folder	다른 이름으로 저장 시 원본 폴더 사용 # Export As metadata and profile options 	Color Profile	색상 프로필 	EXIF	EXIFdiff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv--- a/crates/ui-egui/src/i18n/nl.tsv+++ b/crates/ui-egui/src/i18n/nl.tsv@@ -4059,6 +4059,7 @@ curves	Smooth	Vloeiend maken 	Chisel Soft	Beitel zacht 	Open color-less EXR as ACEScg	EXR zonder kleurruimte als ACEScg openen 	Do not append "copy" to filename when saving a copy	Geen "copy" aan de bestandsnaam toevoegen bij het opslaan van een kopie+	Save As to Original Folder	Opslaan als in oorspronkelijke map  # View › Guides › right-click a guide 	Edit Selected Guides	Geselecteerde hulplijnen bewerkendiff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv--- a/crates/ui-egui/src/i18n/pl.tsv+++ b/crates/ui-egui/src/i18n/pl.tsv@@ -3914,6 +3914,7 @@ curves	Smooth	Wygładź 	Chisel Soft	Dłuto miękkie 	Open color-less EXR as ACEScg	Otwieraj EXR bez profilu koloru jako ACEScg 	Do not append "copy" to filename when saving a copy	Nie dodawaj „copy” do nazwy pliku przy zapisywaniu kopii+	Save As to Original Folder	Zapisz jako w folderze oryginału  # View › Guides › right-click a guide 	Edit Selected Guides	Edytuj zaznaczone prowadnicediff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv--- a/crates/ui-egui/src/i18n/pt-br.tsv+++ b/crates/ui-egui/src/i18n/pt-br.tsv@@ -4089,6 +4089,7 @@ layerLabel	No Color	Sem cor 	Chisel Soft	Cinzel suave 	Open color-less EXR as ACEScg	Abrir EXR sem espaço de cor como ACEScg 	Do not append "copy" to filename when saving a copy	Não acrescentar "copy" ao nome do arquivo ao salvar uma cópia+	Save As to Original Folder	Salvar como na pasta original  # View › Guides › right-click a guide 	Edit Selected Guides	Editar guias selecionadasdiff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv--- a/crates/ui-egui/src/i18n/ru.tsv+++ b/crates/ui-egui/src/i18n/ru.tsv@@ -3273,6 +3273,7 @@ curves	Smooth	Сгладить 	Chisel Soft	Мягкая огранка 	Open color-less EXR as ACEScg	Открывать EXR без цветового профиля как ACEScg 	Do not append "copy" to filename when saving a copy	Не добавлять «copy» к имени файла при сохранении копии+	Save As to Original Folder	«Сохранить как» в исходную папку  # View › Guides › right-click a guide 	Edit Selected Guides	Изменить выбранные направляющиеdiff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv--- a/crates/ui-egui/src/i18n/sr.tsv+++ b/crates/ui-egui/src/i18n/sr.tsv@@ -4330,6 +4330,7 @@ shape	Appearance	Izgled 	Chisel Soft	Meko klesanje 	Open color-less EXR as ACEScg	Otvori EXR bez podatka o bojama kao ACEScg 	Do not append "copy" to filename when saving a copy	Ne dodaji „copy“ u ime fajla pri čuvanju kopije+	Save As to Original Folder	Sačuvaj kao u originalnu fasciklu 	Show home recent files	Prikaži nedavne datoteke na početnom ekranu  # View › Guides › right-click a guidediff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv--- a/crates/ui-egui/src/i18n/tr.tsv+++ b/crates/ui-egui/src/i18n/tr.tsv@@ -4002,6 +4002,7 @@ cameraData	Lens	Lens 	Chisel Soft	Yumuşak Keski 	Open color-less EXR as ACEScg	Renk bilgisi olmayan EXR'yi ACEScg olarak aç 	Do not append "copy" to filename when saving a copy	Kopya kaydederken dosya adına "copy" ekleme+	Save As to Original Folder	Farklı Kaydet’i özgün klasörde başlat  # View › Guides › right-click a guide 	Edit Selected Guides	Seçili Kılavuzları Düzenlediff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv--- a/crates/ui-egui/src/i18n/uk.tsv+++ b/crates/ui-egui/src/i18n/uk.tsv@@ -3999,6 +3999,7 @@ curves	Smooth	Згладити 	Chisel Soft	М'яке різьблення 	Open color-less EXR as ACEScg	Відкривати EXR без колірного профілю як ACEScg 	Do not append "copy" to filename when saving a copy	Не додавати «copy» до імені файлу під час збереження копії+	Save As to Original Folder	«Зберегти як» в початкову папку  # View › Guides › right-click a guide 	Edit Selected Guides	Редагувати вибрані напрямніdiff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv--- a/crates/ui-egui/src/i18n/vi.tsv+++ b/crates/ui-egui/src/i18n/vi.tsv@@ -3762,6 +3762,7 @@ curves	Smooth	Làm mượt 	Chisel Soft	Đục mềm 	Open color-less EXR as ACEScg	Mở EXR không khai báo màu sắc dưới dạng ACEScg 	Do not append "copy" to filename when saving a copy	Không thêm "copy" vào tên tệp khi lưu bản sao+	Save As to Original Folder	Lưu dưới dạng vào thư mục gốc  # View › Guides › right-click a guide 	Edit Selected Guides	Sửa đường căn đã chọndiff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv--- a/crates/ui-egui/src/i18n/zh-hans.tsv+++ b/crates/ui-egui/src/i18n/zh-hans.tsv@@ -2950,6 +2950,7 @@ curves	Smooth	平滑 	Chisel Soft	雕刻柔和 	Open color-less EXR as ACEScg	将无色彩空间信息的 EXR 以 ACEScg 打开 	Do not append "copy" to filename when saving a copy	存储副本时不在文件名后添加“copy”+	Save As to Original Folder	“存储为”到原始文件夹 # Export As metadata and profile options 	Color Profile	颜色配置文件 	EXIF	EXIFdiff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv--- a/crates/ui-egui/src/i18n/zh-hant.tsv+++ b/crates/ui-egui/src/i18n/zh-hant.tsv@@ -2914,6 +2914,7 @@ curves	Smooth	平滑 	Chisel Soft	雕刻柔和 	Open color-less EXR as ACEScg	將無色彩空間資訊的 EXR 以 ACEScg 開啟 	Do not append "copy" to filename when saving a copy	儲存副本時不在檔名後加上「copy」+	Save As to Original Folder	「另存新檔」至原始資料夾 # Export As metadata and profile options 	Color Profile	色彩描述檔 	EXIF	EXIFdiff --git a/crates/ui-egui/src/prefs/prefs_ui/fields.rs b/crates/ui-egui/src/prefs/prefs_ui/fields.rs--- a/crates/ui-egui/src/prefs/prefs_ui/fields.rs+++ b/crates/ui-egui/src/prefs/prefs_ui/fields.rs@@ -60,6 +60,9 @@ pub(super) fn humanize(key: &str) -> String {     // Performance: presentation without vsync (#1343); like the other graphics settings, it     // takes effect when the window next opens.     // File Handling: Photoshop's wording (#8080).+    if key == "saveAsToOriginalFolder" {+        return "Save As to Original Folder".into();+    }     if key == "doNotAppendCopy" {         return "Do not append \"copy\" to filename when saving a copy".into();     }@@ -351,6 +354,8 @@ pub(super) fn field_visible(section: &str, obj: &Map<String, Value>, k: &str, qu     let drawn_elsewhere = (section == "interface" && matches!(k, "theme" | "appearanceMode" | "darkTheme" | "lightTheme" | "favoriteThemes"))         // View › Rulers (⌘R) sets it, as in Photoshop.         || path == "unitsAndRulers.showRulers"+        // The folder of the last save, kept for Save As to Original Folder (#9656).+        || path == "fileHandling.lastSaveFolder"         || (section == "performance" && matches!(k, "useGpu" | "gpuBackend" | "gpuAdapter" | "renderingMode" | "memoryUsageMb"))         // The browser presents frames itself: Low-Latency Drawing does nothing there.         || (cfg!(target_arch = "wasm32") && path == "performance.lowLatencyDrawing")diff --git a/crates/ui-egui/src/prefs/prefs_ui/tests.rs b/crates/ui-egui/src/prefs/prefs_ui/tests.rs--- a/crates/ui-egui/src/prefs/prefs_ui/tests.rs+++ b/crates/ui-egui/src/prefs/prefs_ui/tests.rs@@ -942,6 +942,21 @@ fn ui_font_size_commands_follow_themes_and_dpi_and_reset_live() {     assert!((ctx.pixels_per_point() - 1.5).abs() < 1e-4, "resetting font size keeps UI Scale"); } +#[test]+fn the_last_save_folder_survives_a_restart() {+    let (mut app, store) = app_with_store();+    let ctx = egui::Context::default();+    tick(&mut app, &ctx);+    app.run("prefs.set", json!({"values": {"fileHandling.saveAsToOriginalFolder": false}})).unwrap();+    app.session.prefs.edit(|p| p.file_handling.last_save_folder = "/exports".into());+    tick(&mut app, &ctx);+    let saved = store.lock().unwrap().clone().unwrap();+    let (mut app2, _) = app_with_saved(Some(saved));+    tick(&mut app2, &ctx);+    assert_eq!(app2.session.prefs().file_handling.last_save_folder, "/exports");+    assert_eq!(app2.last_save_dir(), Some(std::path::PathBuf::from("/exports")));+}+ #[test] fn recent_files_survive_a_restart_and_honour_the_count() {     let (mut app, store) = app_with_store();diff --git a/docs/scorecard.md b/docs/scorecard.md--- a/docs/scorecard.md+++ b/docs/scorecard.md@@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js | [Performance](#performance) | 3 | 22 | 31 | 65 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 27 not run, 4 not measurable yet (baseline 2026-10-05) | | [File compatibility](#file-compatibility) | 11 | 14 | 2 | io corpus: oracle ≥ 148/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 249/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 190/258, round trip ≥ 258/258 | | [Tools](#tools) | 36 | 9 | 0 |  |-| [Workspace and UI](#workspace-and-ui) | 15 | 6 | 2 | settings that do nothing: 12 of 178 (target 0) |+| [Workspace and UI](#workspace-and-ui) | 15 | 6 | 2 | settings that do nothing: 12 of 180 (target 0) | | [Type](#type) | 5 | 5 | 0 |  | | [Automation](#automation) | 5 | 2 | 4 | 631 of 631 menu items live (100.0%) | | [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 30 of 30 crates; panic_hunt present |@@ -187,7 +187,7 @@ Corpus floors (`crates/io/tests/it/corpus.rs`): the enforced minimums; the corpu  ## Workspace and UI -**Settings that do nothing: 12 of 178** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.+**Settings that do nothing: 12 of 180** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.  Unread: `general.color_picker`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `performance.gpu_adapter`, `performance.legacy_compositing`, `scratch_disks.disks`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `type_.text_engine`, `enhanced_controls.scrubby_slider_acceleration`.