mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
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:
1 parent
253f0c8497
commit
c87a38546c
4 files changed
+110
-7
No files matched your search
@@ -141,8 +141,11 @@
|
||||
Cut Вирізати
|
||||
Cyan Блакитний
|
||||
Cylinder Циліндр
|
||||
DWG DWG
|
||||
DXF DXF
|
||||
Dark Темна
|
||||
Default Типово
|
||||
Default Save Format Типовий формат збереження
|
||||
Define Attributes Визначити атрибути
|
||||
Define Attributes... Визначити атрибути...
|
||||
Defined width Задана ширина
|
||||
|
||||
|
@@ -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,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))
|
||||
|
||||
@@ -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?}`. |
|
||||
|
||||
Reference in new issue
Block a user