Save As suggests the default save format for opened drawings too (#2199) (#2509)

* Save As suggests the default save format for opened drawings too (#2199)

Options > Open and Save > "Save as" in AutoCAD is the format every drawing
is saved in. CADCraft applied File > Default Save Format only to untitled
drawings, so Save As on an opened DWG kept DWG with DXF chosen, and the
other way round. Save As now swaps a DWG, DXF or DWT extension for the
chosen format; other names, and Save of an opened file, are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Default Save Format's echo says Save As will suggest the format (#2199)

"Save As will save drawings as DWG" promised more than Save As does: it
only swaps a DWG, DXF or DWT extension in the name it suggests, and the
path picked in the dialog decides the format. The echo now reads
"Save As will suggest DWG", and default_save_format_applies_to_opened_drawings
pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zinadin Zidan
2026-10-30 15:46:34 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 43c41b8e68
commit 8d1bd19ee2
3 changed files with 59 additions and 13 deletions
+24 -12
View File
@@ -118,7 +118,7 @@ pub struct UiState {
/// Animation effects (dialog and menu fades, smoothed scrolling): "system" (follow the OS
/// setting), "on" or "off".
pub animations: animation::AnimationPref,
/// The format Save As suggests for a drawing that has no file yet: "dxf" or "dwg".
/// The format Save As suggests: "dxf" or "dwg", for new drawings and opened files alike.
pub save_format: SaveFormat,
/// The version Save As writes for a drawing that has no file yet ("2000" … "2018"); a
/// drawing opened from a file keeps that file's version.
@@ -132,8 +132,8 @@ pub struct UiState {
/// A DXF/DWG file version (2000 … 2018).
pub use cadcraft_engine::cmd::file::Version as SaveVersion;
/// The default save format for new drawings, like AutoCAD's Options > Open and Save > "Save as".
/// A drawing opened from a file keeps that file's format.
/// The default save format, like AutoCAD's Options > Open and Save > "Save as": the format Save As
/// suggests for every drawing, an opened DWG or DXF too. Save (QSAVE) of an opened file keeps its file.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SaveFormat {
@@ -158,13 +158,19 @@ impl SaveFormat {
SaveFormat::ALL.into_iter().find(|f| f.as_str().eq_ignore_ascii_case(s.trim().trim_start_matches('.')))
}
/// The name Save As suggests: a title that already has an extension (a drawing opened from a
/// file) keeps it; an untitled drawing gets this format's extension.
/// The name Save As suggests: this format's extension, replacing a drawing's own (DWG, DXF or
/// DWT, from a drawing opened from a file); any other extension is kept.
pub fn suggested_name(self, title: Option<&str>) -> String {
match title.map(str::trim).filter(|t| !t.is_empty()) {
Some(t) if std::path::Path::new(t).extension().is_some() => t.to_owned(),
Some(t) => format!("{t}.{}", self.as_str()),
None => format!("Drawing.{}", self.as_str()),
let Some(t) = title.map(str::trim).filter(|t| !t.is_empty()) else { return format!("Drawing.{}", self.as_str()) };
let path = std::path::Path::new(t);
match path.extension().and_then(|e| e.to_str()) {
Some(e) if ["dwg", "dxf", "dwt"].iter().any(|d| d.eq_ignore_ascii_case(e)) => {
// the extension is ASCII, so this is a char boundary
let stem = t.get(..t.len().saturating_sub(e.len() + 1)).unwrap_or(t);
format!("{stem}.{}", self.as_str())
}
Some(_) => t.to_owned(),
None => format!("{t}.{}", self.as_str()),
}
}
}
@@ -1142,11 +1148,17 @@ mod tests {
b.start("ui.saveformat.dwg");
b.start("ui.resetpalettes");
assert_eq!(b.ui.save_format, SaveFormat::Dwg);
// Untitled drawings get the format's extension; files keep their own.
// Untitled drawings get the format's extension, and so do opened drawings (#2199).
assert_eq!(SaveFormat::Dwg.suggested_name(Some("Drawing1")), "Drawing1.dwg");
assert_eq!(SaveFormat::Dxf.suggested_name(Some("Drawing1")), "Drawing1.dxf");
assert_eq!(SaveFormat::Dwg.suggested_name(Some("Floorplan.dxf")), "Floorplan.dxf");
assert_eq!(SaveFormat::Dxf.suggested_name(Some("Floorplan.DWG")), "Floorplan.DWG");
assert_eq!(SaveFormat::Dwg.suggested_name(Some("Floorplan.dxf")), "Floorplan.dwg");
assert_eq!(SaveFormat::Dxf.suggested_name(Some("Floorplan.DWG")), "Floorplan.dxf");
assert_eq!(SaveFormat::Dxf.suggested_name(Some("Floorplan.dxf")), "Floorplan.dxf");
assert_eq!(SaveFormat::Dwg.suggested_name(Some("Title block.dwt")), "Title block.dwg");
assert_eq!(SaveFormat::Dwg.suggested_name(Some("v1.2 plan.Dxf")), "v1.2 plan.dwg");
assert_eq!(SaveFormat::Dwg.suggested_name(Some("平面図.dxf")), "平面図.dwg");
assert_eq!(SaveFormat::Dwg.suggested_name(Some(".dxf")), ".dxf.dwg", "a dot file has no extension");
assert_eq!(SaveFormat::Dxf.suggested_name(Some("logo.svg")), "logo.svg", "other files keep their type");
assert_eq!(SaveFormat::Dwg.suggested_name(None), "Drawing.dwg");
assert_eq!(SaveFormat::Dwg.suggested_name(Some(" ")), "Drawing.dwg");
let ui: UiState = serde_json::from_value(json!({ "saveFormat": "dwg" })).unwrap_or_default();
+1 -1
View File
@@ -520,7 +520,7 @@ fn saveas_params(ui: &crate::UiState, session: &cadcraft_engine::Session, path:
fn set_save_format(app: &mut CadApp, format: crate::SaveFormat) -> Value {
app.ui.save_format = format;
app.session.echo(format!("New drawings will be saved as {}", format.as_str().to_ascii_uppercase()));
app.session.echo(format!("Save As will suggest {}", format.as_str().to_ascii_uppercase()));
json!({ "saveFormat": format.as_str() })
}
+34
View File
@@ -44,3 +44,37 @@ fn default_save_version_applies_to_new_drawings_and_persists() {
b.start("ui.resetpalettes");
assert_eq!(b.ui.save_version, "2004");
}
/// The default save format (#2199) is what Save As suggests for an opened DWG or DXF too, as
/// AutoCAD's Options > Open and Save > "Save as" does; Save keeps the opened file.
#[test]
fn default_save_format_applies_to_opened_drawings() {
set_io(IoHooks { read: |_, _| Ok(Drawing::new_metric()), write: |_, _| Ok(b"plain".to_vec()), plot: None });
set_write_as(with_options);
let dir = std::env::temp_dir().join(format!("cadcraft-ui-save-format-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let suggested = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let (seen, to) = (suggested.clone(), dir.clone());
let pick = move |name: &str| {
seen.lock().unwrap().push(name.to_string());
Some(to.join(name).to_string_lossy().to_string())
};
let mut app = CadApp::new(Session::empty(), Services { pick_save: Some(Box::new(pick)), ..Services::default() });
for (opened, format, expected) in
[("Plan.dwg", "dxf", "Plan.dxf"), ("Plan.DXF", "dwg", "Plan.dwg"), ("Plan.dxf", "dxf", "Plan.dxf"), ("Sheet.dwt", "dwg", "Sheet.dwg")]
{
let file = dir.join(opened);
std::fs::write(&file, b"0\nEOF\n").unwrap();
app.run("open", json!({ "path": file.to_string_lossy() })).unwrap();
app.run("ui.saveformat", json!({ "format": format })).unwrap();
// the picker's path decides the format, so the echo promises only the suggestion
let echo = format!("Save As will suggest {}", format.to_ascii_uppercase());
assert_eq!(app.session.log.last(), Some(&echo), "{opened} with {format}");
app.run("ui.saveas", Value::Null).unwrap();
assert_eq!(suggested.lock().unwrap().last().map(String::as_str), Some(expected), "{opened} with {format}");
let path = app.session.state().unwrap().path.clone().unwrap_or_default();
assert!(path.ends_with(expected), "{opened}: saved as {path}");
assert!(dir.join(expected).exists());
}
let _ = std::fs::remove_dir_all(&dir);
}