Default save format for new drawings (DXF or DWG) (#298)

Save As suggested `.dxf` for every untitled drawing, so saving new work as
DWG meant typing the extension each time. AutoCAD keeps this choice in
Options > Open and Save > "Save as".

- File > Default Save Format > DXF / DWG sets the format Save As suggests
  for drawings that have no file yet. Drawings opened from a file keep that
  file's format, as before. DXF stays the default.
- The choice is saved across restarts with the other preferences
  (`saveFormat`), survives Reset Palettes, and is scriptable as
  `ui.saveformat {format}` or the `saveFormat` UiState field.
- Ukrainian labels for the new menu entries.

On macOS the suggested `.dwg` name needs DWG in the Save As filter list
(#198) to survive the dialog.


Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
by TruittandClaude Opus 5.5 authored and GitHub committed 2026-10-10 16:11:46 -04:00
1 parent 253f0c8497
commit c87a38546c
4 files changed
+110 -7

No files matched your search

+3
View File
@@ -141,8 +141,11 @@
Cut Вирізати
Cyan Блакитний
Cylinder Циліндр
DWG DWG
DXF DXF
Dark Темна
Default Типово
Default Save Format Типовий формат збереження
Define Attributes Визначити атрибути
Define Attributes... Визначити атрибути...
Defined width Задана ширина
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
141
142
143
144
145
146
147
148
149
150
151
+80 -2
View File
@@ -69,6 +69,45 @@ pub struct UiState {
pub history_lines: usize,
/// Interface theme: "system" (follow the OS light/dark appearance), "light" or "dark".
pub theme: theme::ThemePref,
/// The format Save As suggests for a drawing that has no file yet: "dxf" or "dwg".
pub save_format: SaveFormat,
}
/// 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.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SaveFormat {
/// CADCraft's native format, and the default.
#[default]
Dxf,
Dwg,
}
impl SaveFormat {
pub const ALL: [SaveFormat; 2] = [SaveFormat::Dxf, SaveFormat::Dwg];
/// The file extension, which is also the saved name ("dxf" or "dwg").
pub fn as_str(self) -> &'static str {
match self {
SaveFormat::Dxf => "dxf",
SaveFormat::Dwg => "dwg",
}
}
pub fn parse(s: &str) -> Option<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.
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()),
}
}
}
impl Default for UiState {
@@ -93,6 +132,7 @@ impl Default for UiState {
dialog: None,
history_lines: 3,
theme: theme::ThemePref::default(),
save_format: SaveFormat::default(),
}
}
}
@@ -226,7 +266,12 @@ impl CadApp {
/// Preferences kept across restarts, as JSON for the host's storage ([`PREFS_KEY`]).
pub fn prefs_json(&self) -> String {
json!({ "theme": self.ui.theme.as_str(), "interfaceLanguage": self.ui.interface_language.code() }).to_string()
json!({
"theme": self.ui.theme.as_str(),
"interfaceLanguage": self.ui.interface_language.code(),
"saveFormat": self.ui.save_format.as_str(),
})
.to_string()
}
/// Restore preferences saved by [`Self::prefs_json`]; unknown or malformed values are ignored.
@@ -238,6 +283,9 @@ impl CadApp {
if let Some(l) = v.get("interfaceLanguage").and_then(Value::as_str).and_then(i18n::Preference::parse) {
self.ui.interface_language = l;
}
if let Some(f) = v.get("saveFormat").and_then(Value::as_str).and_then(SaveFormat::parse) {
self.ui.save_format = f;
}
}
pub fn set_status(&mut self, s: impl Into<String>) {
@@ -524,6 +572,36 @@ mod tests {
frame(&mut a, &ctx, None);
assert_eq!(Tokens::get(), Tokens::of(theme::SYSTEM_FALLBACK), "no OS appearance: the documented fallback");
assert_eq!(a.ui.theme, ThemePref::System);
assert_eq!(a.prefs_json(), r#"{"interfaceLanguage":"auto","theme":"system"}"#);
assert_eq!(a.prefs_json(), r#"{"interfaceLanguage":"auto","saveFormat":"dxf","theme":"system"}"#);
}
#[test]
fn default_save_format_persists_and_names_new_drawings() {
let mut a = app();
assert_eq!(a.ui.save_format, SaveFormat::Dxf, "default keeps DXF, CADCraft's native format");
assert!(a.run("ui.saveformat", json!({ "format": "pdf" })).is_err());
assert_eq!(a.run("ui.saveformat", json!({ "format": "DWG" })).ok(), Some(json!({ "saveFormat": "dwg" })));
assert_eq!(a.run("ui.saveformat", json!({})).ok(), Some(json!({ "saveFormat": "dwg" })), "no format: report it");
// Saved and restored by the host; bad values are ignored.
let mut b = app();
b.load_prefs(&a.prefs_json());
assert_eq!(b.ui.save_format, SaveFormat::Dwg);
b.load_prefs(r#"{"saveFormat": "svg"}"#);
assert_eq!(b.ui.save_format, SaveFormat::Dwg);
// The menu items reach the same setting, and resetting the palettes keeps it.
b.start("ui.saveformat.dxf");
assert_eq!(b.ui.save_format, SaveFormat::Dxf);
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.
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(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();
assert_eq!(ui.save_format, SaveFormat::Dwg);
}
}
+26 -4
View File
@@ -26,6 +26,9 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
("ui.theme.light", "Light", &["View", "Interface Theme", "Light"], None),
("ui.theme.dark", "Dark", &["View", "Interface Theme", "Dark"], None),
("ui.theme", "Interface Theme", &[], None),
("ui.saveformat.dxf", "DXF", &["File", "Default Save Format", "DXF"], None),
("ui.saveformat.dwg", "DWG", &["File", "Default Save Format", "DWG"], None),
("ui.saveformat", "Default Save Format", &[], None),
("ui.toggle.menubar", "In-window Menu Bar", &["Window", "In-window Menu Bar"], None),
("ui.hidepalettes", "Hide Palettes", &["Window", "Hide Palettes"], None),
("ui.resetpalettes", "Reset Palettes", &["Window", "Reset Palettes"], None),
@@ -82,8 +85,8 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
Ok(Value::Null)
}
"ui.saveas" | "saveas" if no_path => {
let name = app.session.state().map(|s| s.title.clone()).unwrap_or_else(|_| "Drawing.dxf".into());
let name = if name.contains('.') { name } else { format!("{name}.dxf") };
let title = app.session.state().map(|s| s.title.clone()).ok();
let name = app.ui.save_format.suggested_name(title.as_deref());
if let Some(p) = app.services.pick_save.as_ref().and_then(|f| f(&name)) {
return Some(app.session.execute("saveas", &json!({ "path": p })).map_err(|e| e.to_string()));
}
@@ -164,14 +167,27 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
},
None => Ok(theme_json(app)),
},
"ui.saveformat.dxf" | "ui.saveformat.dwg" => {
let format = crate::SaveFormat::parse(id.trim_start_matches("ui.saveformat.")).unwrap_or_default();
Ok(set_save_format(app, format))
}
// `{"format": "dxf"|"dwg"}` sets the default save format for new drawings; without it,
// reports it.
"ui.saveformat" => match params.get("format").and_then(Value::as_str) {
Some(name) => match crate::SaveFormat::parse(name) {
Some(format) => Ok(set_save_format(app, format)),
None => Err(format!("unknown save format \"{name}\" (dxf or dwg)")),
},
None => Ok(json!({ "saveFormat": app.ui.save_format.as_str() })),
},
"ui.hidepalettes" => {
app.ui.show_palettes = false;
app.ui.show_toolsets = false;
Ok(Value::Null)
}
"ui.resetpalettes" => {
let (menu, theme, interface_language) = (app.ui.in_window_menu, app.ui.theme, app.ui.interface_language);
app.ui = crate::UiState { in_window_menu: menu, theme, interface_language, ..Default::default() };
let (menu, theme, interface_language, save_format) = (app.ui.in_window_menu, app.ui.theme, app.ui.interface_language, app.ui.save_format);
app.ui = crate::UiState { in_window_menu: menu, theme, interface_language, save_format, ..Default::default() };
Ok(Value::Null)
}
"ui.start" => {
@@ -256,6 +272,12 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
Some(r)
}
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()));
json!({ "saveFormat": format.as_str() })
}
fn set_theme(app: &mut CadApp, pref: crate::theme::ThemePref) -> Result<Value, String> {
app.ui.theme = pref;
Ok(theme_json(app))
+1 -1
View File
@@ -25,7 +25,7 @@ Errors come back as `{"id": …, "ok": false, "error": "message"}`.
| `ui.click` / `ui.move` | `{x, y, button?, shift?}` | Real egui pointer events in screen points (reach every widget). |
| `ui.drag` | `{x, y, to: [x, y], button?}` | Real press, move and release (drag the command line's top edge, the ViewCube ring…). |
| `ui.key` / `ui.text` | `{key, cmd?, shift?, alt?}` / `{text}` | Synthetic keyboard input. |
| `ui.set` | UiState fields | Show/hide palettes, toolbars, command line, ViewCube…; `theme: "system"\|"light"\|"dark"` (also `engine.execute` with `ui.theme {theme}`); `ui.inspect` reports the theme shown (`theme`: light/dark). |
| `ui.set` | UiState fields | Show/hide palettes, toolbars, command line, ViewCube…; `theme: "system"\|"light"\|"dark"` (also `engine.execute` with `ui.theme {theme}`); `ui.inspect` reports the theme shown (`theme`: light/dark). `saveFormat: "dxf"\|"dwg"` (also `ui.saveformat {format}`) is the format Save As suggests for new drawings. |
| `ui.resize` | `{width, height}` | Resize the window. |
| `ui.screenshot` | `{path?}` | PNG of the window (needs a presented frame). |
| `ui.render` | `{path?, width?, height?, fit?}` | Headless render. `fit` (default true) frames the whole drawing; `false` uses the current view. Replies `{pngBase64, width, height, bytes, path?}`. |