fix: replace broken version filters with custom Save As dialog
rfd does not expose the selected filter index, so the old DWG/DXF version filters had no effect — every file was always saved as AC1032 (R2018). Replace with a single custom iced Save As window that shows: - File name text input - Save-in folder input + Browse button (rfd folder picker) - Format dropdown: DWG/DXF × 2018/2013/2010/2007/2004/2000/R14 - Save / Cancel buttons The selected format is passed to save_as_version() which temporarily sets doc.version before writing, so DwgWriter/DxfWriter emit the correct file format. Also works in the unsaved-changes Save flow. Closes #9 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9974456a5d
commit
9cd8eff77f
4 changed files with 307 additions and 66 deletions
|
|
@ -153,6 +153,18 @@ pub(super) struct H7CAD {
|
||||||
/// OS window for the unsaved-changes confirmation dialog.
|
/// OS window for the unsaved-changes confirmation dialog.
|
||||||
unsaved_dialog_window: Option<window::Id>,
|
unsaved_dialog_window: Option<window::Id>,
|
||||||
|
|
||||||
|
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
||||||
|
/// OS window for the custom Save As dialog (format + filename + folder).
|
||||||
|
save_dialog_window: Option<window::Id>,
|
||||||
|
/// Currently selected format string, e.g. "DWG 2013".
|
||||||
|
save_dialog_format: String,
|
||||||
|
/// Editable filename (without path), e.g. "drawing.dwg".
|
||||||
|
save_dialog_filename: String,
|
||||||
|
/// Target folder path (as a string for editing).
|
||||||
|
save_dialog_folder: String,
|
||||||
|
/// True when triggered from the unsaved-changes flow.
|
||||||
|
save_dialog_for_unsaved: bool,
|
||||||
|
|
||||||
// ── DimStyle Dialog ───────────────────────────────────────────────────
|
// ── DimStyle Dialog ───────────────────────────────────────────────────
|
||||||
/// Name of the style currently shown in the dialog.
|
/// Name of the style currently shown in the dialog.
|
||||||
dimstyle_selected: String,
|
dimstyle_selected: String,
|
||||||
|
|
@ -201,6 +213,14 @@ pub enum Message {
|
||||||
SaveFile,
|
SaveFile,
|
||||||
SaveAs,
|
SaveAs,
|
||||||
PickedSavePath(Option<PathBuf>),
|
PickedSavePath(Option<PathBuf>),
|
||||||
|
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
||||||
|
SaveDialogFormatChanged(String),
|
||||||
|
SaveDialogFilenameChanged(String),
|
||||||
|
SaveDialogFolderChanged(String),
|
||||||
|
SaveDialogBrowse,
|
||||||
|
SaveDialogFolderPicked(Option<std::path::PathBuf>),
|
||||||
|
SaveDialogConfirm,
|
||||||
|
SaveDialogCancel,
|
||||||
ClearScene,
|
ClearScene,
|
||||||
SetWireframe(bool),
|
SetWireframe(bool),
|
||||||
/// Switch camera projection: true = Orthographic, false = Perspective.
|
/// Switch camera projection: true = Orthographic, false = Perspective.
|
||||||
|
|
@ -624,6 +644,13 @@ impl H7CAD {
|
||||||
page_setup_scale: "Fit".to_string(),
|
page_setup_scale: "Fit".to_string(),
|
||||||
pending_close: None,
|
pending_close: None,
|
||||||
unsaved_dialog_window: None,
|
unsaved_dialog_window: None,
|
||||||
|
save_dialog_window: None,
|
||||||
|
save_dialog_format: "DWG 2018".to_string(),
|
||||||
|
save_dialog_filename: "drawing.dwg".to_string(),
|
||||||
|
save_dialog_folder: std::env::var("HOME")
|
||||||
|
.or_else(|_| std::env::var("USERPROFILE"))
|
||||||
|
.unwrap_or_else(|_| ".".to_string()),
|
||||||
|
save_dialog_for_unsaved: false,
|
||||||
// Plot style
|
// Plot style
|
||||||
active_plot_style: None,
|
active_plot_style: None,
|
||||||
// Color scheme (default: dark CAD-style)
|
// Color scheme (default: dark CAD-style)
|
||||||
|
|
@ -705,6 +732,7 @@ pub fn run() -> iced::Result {
|
||||||
if Some(window_id) == state.shortcuts_window { return "Keyboard Shortcuts".into(); }
|
if Some(window_id) == state.shortcuts_window { return "Keyboard Shortcuts".into(); }
|
||||||
if Some(window_id) == state.about_window { return "About H7CAD".into(); }
|
if Some(window_id) == state.about_window { return "About H7CAD".into(); }
|
||||||
if Some(window_id) == state.unsaved_dialog_window { return "Unsaved Changes".into(); }
|
if Some(window_id) == state.unsaved_dialog_window { return "Unsaved Changes".into(); }
|
||||||
|
if Some(window_id) == state.save_dialog_window { return "Save As".into(); }
|
||||||
if let Some(tab) = state.tabs.get(state.active_tab) {
|
if let Some(tab) = state.tabs.get(state.active_tab) {
|
||||||
let dot = if tab.dirty { "● " } else { "" };
|
let dot = if tab.dirty { "● " } else { "" };
|
||||||
let name = tab.tab_display_name();
|
let name = tab.tab_display_name();
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ impl H7CAD {
|
||||||
let name = block_name.clone();
|
let name = block_name.clone();
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
let path = crate::io::pick_save_path().await;
|
let path = crate::io::pick_folder("").await.map(|f| f.join("drawing.dwg"));
|
||||||
(name, path)
|
(name, path)
|
||||||
},
|
},
|
||||||
|(name, path)| Message::WblockSaveResult(name, path),
|
|(name, path)| Message::WblockSaveResult(name, path),
|
||||||
|
|
@ -411,29 +411,94 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
||||||
}
|
}
|
||||||
|
Task::none()
|
||||||
} else {
|
} else {
|
||||||
return Task::perform(crate::io::pick_save_path(), Message::PickedSavePath);
|
self.save_dialog_for_unsaved = false;
|
||||||
|
self.open_save_dialog_window(i)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SaveAs => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
self.save_dialog_for_unsaved = false;
|
||||||
|
self.open_save_dialog_window(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SaveDialogFormatChanged(fmt) => {
|
||||||
|
// Keep filename extension in sync with format choice.
|
||||||
|
let (ext, _) = crate::io::parse_save_format(&fmt);
|
||||||
|
let stem = std::path::Path::new(&self.save_dialog_filename)
|
||||||
|
.file_stem()
|
||||||
|
.map(|s| s.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "drawing".to_string());
|
||||||
|
self.save_dialog_filename = format!("{stem}.{ext}");
|
||||||
|
self.save_dialog_format = fmt;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::SaveAs => Task::perform(crate::io::pick_save_path(), Message::PickedSavePath),
|
Message::SaveDialogFilenameChanged(name) => {
|
||||||
|
self.save_dialog_filename = name;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::PickedSavePath(Some(path)) => {
|
Message::SaveDialogFolderChanged(folder) => {
|
||||||
let i = self.active_tab;
|
self.save_dialog_folder = folder;
|
||||||
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SaveDialogBrowse => {
|
||||||
|
let folder = self.save_dialog_folder.clone();
|
||||||
|
Task::perform(
|
||||||
|
async move { crate::io::pick_folder(&folder).await },
|
||||||
|
Message::SaveDialogFolderPicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SaveDialogFolderPicked(Some(p)) => {
|
||||||
|
self.save_dialog_folder = p.to_string_lossy().into_owned();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SaveDialogFolderPicked(None) => Task::none(),
|
||||||
|
|
||||||
|
Message::SaveDialogConfirm => {
|
||||||
|
let folder = std::path::Path::new(&self.save_dialog_folder);
|
||||||
|
let path = folder.join(&self.save_dialog_filename);
|
||||||
|
let (_, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
||||||
|
let close = self.close_save_dialog_window();
|
||||||
|
if self.save_dialog_for_unsaved {
|
||||||
|
let result = crate::io::save_as_version(
|
||||||
|
&self.tabs[self.active_tab].scene.document, &path, version,
|
||||||
|
);
|
||||||
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.command_line
|
let i = self.active_tab;
|
||||||
.push_output(&format!("Saved: {}", path.display()));
|
self.command_line.push_output(&format!("Saved: {}", path.display()));
|
||||||
|
self.tabs[i].current_path = Some(path.clone());
|
||||||
|
self.tabs[i].dirty = false;
|
||||||
|
let close_tab_or_quit = self.update(Message::UnsavedPickedSavePath(Some(path)));
|
||||||
|
return Task::batch([close, close_tab_or_quit]);
|
||||||
|
}
|
||||||
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
||||||
|
}
|
||||||
|
close
|
||||||
|
} else {
|
||||||
|
let i = self.active_tab;
|
||||||
|
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
|
||||||
|
Ok(()) => {
|
||||||
|
self.command_line.push_output(&format!("Saved: {}", path.display()));
|
||||||
self.tabs[i].current_path = Some(path);
|
self.tabs[i].current_path = Some(path);
|
||||||
self.tabs[i].dirty = false;
|
self.tabs[i].dirty = false;
|
||||||
}
|
}
|
||||||
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
||||||
}
|
}
|
||||||
Task::none()
|
close
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::PickedSavePath(None) => Task::none(),
|
Message::SaveDialogCancel => self.close_save_dialog_window(),
|
||||||
|
|
||||||
|
Message::PickedSavePath(Some(_)) | Message::PickedSavePath(None) => Task::none(),
|
||||||
|
|
||||||
Message::ClearScene => {
|
Message::ClearScene => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|
@ -731,6 +796,10 @@ impl H7CAD {
|
||||||
self.pending_close = None;
|
self.pending_close = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
if self.save_dialog_window == Some(id) {
|
||||||
|
self.save_dialog_window = None;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
if self.layer_window == Some(id) { self.layer_window = None; }
|
if self.layer_window == Some(id) { self.layer_window = None; }
|
||||||
if self.page_setup_window == Some(id) { self.page_setup_window = None; }
|
if self.page_setup_window == Some(id) { self.page_setup_window = None; }
|
||||||
if self.textstyle_window == Some(id) { self.textstyle_window = None; }
|
if self.textstyle_window == Some(id) { self.textstyle_window = None; }
|
||||||
|
|
@ -2756,13 +2825,12 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No path — close dialog, show save-as, then come back.
|
// No path — close unsaved dialog, open custom Save As dialog.
|
||||||
self.pending_close = Some(super::PendingClose::Tab(idx));
|
self.pending_close = Some(super::PendingClose::Tab(idx));
|
||||||
|
self.save_dialog_for_unsaved = true;
|
||||||
let close_win = self.close_unsaved_dialog_window();
|
let close_win = self.close_unsaved_dialog_window();
|
||||||
return Task::batch(vec![
|
let open_save = self.open_save_dialog_window(idx);
|
||||||
close_win,
|
return Task::batch([close_win, open_save]);
|
||||||
Task::perform(crate::io::pick_save_path(), Message::UnsavedPickedSavePath),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(super::PendingClose::Quit) => {
|
Some(super::PendingClose::Quit) => {
|
||||||
|
|
@ -2780,14 +2848,13 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No path — close dialog, show save-as, then come back.
|
// No path — close unsaved dialog, open custom Save As dialog.
|
||||||
self.active_tab = idx;
|
self.active_tab = idx;
|
||||||
self.pending_close = Some(super::PendingClose::Quit);
|
self.pending_close = Some(super::PendingClose::Quit);
|
||||||
|
self.save_dialog_for_unsaved = true;
|
||||||
let close_win = self.close_unsaved_dialog_window();
|
let close_win = self.close_unsaved_dialog_window();
|
||||||
return Task::batch(vec![
|
let open_save = self.open_save_dialog_window(idx);
|
||||||
close_win,
|
return Task::batch([close_win, open_save]);
|
||||||
Task::perform(crate::io::pick_save_path(), Message::UnsavedPickedSavePath),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.tabs.iter().any(|t| t.dirty) {
|
if self.tabs.iter().any(|t| t.dirty) {
|
||||||
|
|
@ -2804,9 +2871,10 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::UnsavedPickedSavePath(Some(path)) => {
|
Message::UnsavedPickedSavePath(Some(path)) => {
|
||||||
|
let (_, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
||||||
match self.pending_close.take() {
|
match self.pending_close.take() {
|
||||||
Some(super::PendingClose::Tab(idx)) => {
|
Some(super::PendingClose::Tab(idx)) => {
|
||||||
match crate::io::save(&self.tabs[idx].scene.document, &path) {
|
match crate::io::save_as_version(&self.tabs[idx].scene.document, &path, version) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.command_line.push_output(&format!("Saved: {}", path.display()));
|
self.command_line.push_output(&format!("Saved: {}", path.display()));
|
||||||
self.tabs[idx].current_path = Some(path);
|
self.tabs[idx].current_path = Some(path);
|
||||||
|
|
@ -2815,7 +2883,6 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.command_line.push_error(&format!("Save failed: {e}"));
|
self.command_line.push_error(&format!("Save failed: {e}"));
|
||||||
// Re-open dialog for retry.
|
|
||||||
self.pending_close = Some(super::PendingClose::Tab(idx));
|
self.pending_close = Some(super::PendingClose::Tab(idx));
|
||||||
return self.open_unsaved_dialog_window();
|
return self.open_unsaved_dialog_window();
|
||||||
}
|
}
|
||||||
|
|
@ -2823,7 +2890,7 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
Some(super::PendingClose::Quit) => {
|
Some(super::PendingClose::Quit) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.command_line.push_output(&format!("Saved: {}", path.display()));
|
self.command_line.push_output(&format!("Saved: {}", path.display()));
|
||||||
self.tabs[i].current_path = Some(path);
|
self.tabs[i].current_path = Some(path);
|
||||||
|
|
@ -3851,6 +3918,41 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populate edit buffers from the currently selected text style.
|
/// Populate edit buffers from the currently selected text style.
|
||||||
|
fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task<Message> {
|
||||||
|
if let Some(id) = self.save_dialog_window {
|
||||||
|
return window::gain_focus(id);
|
||||||
|
}
|
||||||
|
// Pre-fill filename from current path or tab name.
|
||||||
|
let (filename, folder) = if let Some(p) = &self.tabs[tab_idx].current_path {
|
||||||
|
let name = p.file_name().map(|n| n.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "drawing.dwg".to_string());
|
||||||
|
let dir = p.parent().map(|d| d.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| self.save_dialog_folder.clone());
|
||||||
|
(name, dir)
|
||||||
|
} else {
|
||||||
|
let (ext, _) = crate::io::parse_save_format(&self.save_dialog_format);
|
||||||
|
let name = format!("{}.{ext}", self.tabs[tab_idx].tab_display_name());
|
||||||
|
(name, self.save_dialog_folder.clone())
|
||||||
|
};
|
||||||
|
self.save_dialog_filename = filename;
|
||||||
|
self.save_dialog_folder = folder;
|
||||||
|
let (id, task) = window::open(window::Settings {
|
||||||
|
size: iced::Size::new(500.0, 220.0),
|
||||||
|
resizable: false,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
self.save_dialog_window = Some(id);
|
||||||
|
task.map(|_| Message::Noop)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_save_dialog_window(&mut self) -> Task<Message> {
|
||||||
|
if let Some(id) = self.save_dialog_window.take() {
|
||||||
|
window::close(id)
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn open_unsaved_dialog_window(&mut self) -> Task<Message> {
|
fn open_unsaved_dialog_window(&mut self) -> Task<Message> {
|
||||||
if let Some(id) = self.unsaved_dialog_window {
|
if let Some(id) = self.unsaved_dialog_window {
|
||||||
return window::gain_focus(id);
|
return window::gain_focus(id);
|
||||||
|
|
|
||||||
110
src/app/view.rs
110
src/app/view.rs
|
|
@ -7,7 +7,7 @@ use crate::scene::grip::{grips_to_screen, grips_to_screen_paper};
|
||||||
use crate::scene::paper_canvas::PaperCanvas;
|
use crate::scene::paper_canvas::PaperCanvas;
|
||||||
use crate::scene::viewport_pane::{PaperViewportPane, ViewportPane};
|
use crate::scene::viewport_pane::{PaperViewportPane, ViewportPane};
|
||||||
use crate::ui::overlay;
|
use crate::ui::overlay;
|
||||||
use iced::widget::{button, canvas, column, container, mouse_area, row, shader, stack, text, Row, Space};
|
use iced::widget::{button, canvas, column, container, mouse_area, pick_list, row, shader, stack, text, text_input, Row, Space};
|
||||||
use iced::window;
|
use iced::window;
|
||||||
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
||||||
|
|
||||||
|
|
@ -115,6 +115,13 @@ impl H7CAD {
|
||||||
};
|
};
|
||||||
return unsaved_changes_dialog_window(&tab_name);
|
return unsaved_changes_dialog_window(&tab_name);
|
||||||
}
|
}
|
||||||
|
if Some(window_id) == self.save_dialog_window {
|
||||||
|
return save_as_dialog_window(
|
||||||
|
&self.save_dialog_filename,
|
||||||
|
&self.save_dialog_folder,
|
||||||
|
&self.save_dialog_format,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let tab = &self.tabs[i];
|
let tab = &self.tabs[i];
|
||||||
|
|
@ -842,6 +849,107 @@ fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Content for the floating "Unsaved Changes" OS window.
|
/// Content for the floating "Unsaved Changes" OS window.
|
||||||
|
const SAVE_FORMAT_OPTIONS: &[&str] = &[
|
||||||
|
"DWG 2018", "DWG 2013", "DWG 2010", "DWG 2007", "DWG 2004", "DWG 2000", "DWG R14",
|
||||||
|
"DXF 2018", "DXF 2013", "DXF 2010", "DXF 2007", "DXF 2004", "DXF 2000", "DXF R14",
|
||||||
|
];
|
||||||
|
|
||||||
|
fn save_as_dialog_window<'a>(
|
||||||
|
filename: &'a str,
|
||||||
|
folder: &'a str,
|
||||||
|
format: &'a str,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
|
||||||
|
const BORDER_COL:Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 };
|
||||||
|
const TEXT_COL: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
|
||||||
|
const INPUT_BG: Color = Color { r: 0.12, g: 0.12, b: 0.14, a: 1.0 };
|
||||||
|
const BTN_OK: Color = Color { r: 0.20, g: 0.46, b: 0.80, a: 1.0 };
|
||||||
|
const BTN_HOV: Color = Color { r: 0.26, g: 0.55, b: 0.92, a: 1.0 };
|
||||||
|
const BTN_DISC: Color = Color { r: 0.28, g: 0.28, b: 0.30, a: 1.0 };
|
||||||
|
const BTN_DHOV: Color = Color { r: 0.36, g: 0.36, b: 0.40, a: 1.0 };
|
||||||
|
|
||||||
|
let input_style = |_: &Theme, _status: iced::widget::text_input::Status| {
|
||||||
|
iced::widget::text_input::Style {
|
||||||
|
background: Background::Color(INPUT_BG),
|
||||||
|
border: Border { color: BORDER_COL, width: 1.0, radius: 4.0.into() },
|
||||||
|
icon: TEXT_COL,
|
||||||
|
placeholder: Color { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
|
||||||
|
value: TEXT_COL,
|
||||||
|
selection: Color { r: 0.20, g: 0.46, b: 0.80, a: 0.5 },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
|
||||||
|
button(text(label).size(13).color(TEXT_COL))
|
||||||
|
.on_press(msg)
|
||||||
|
.style(move |_: &Theme, status| button::Style {
|
||||||
|
background: Some(Background::Color(match status {
|
||||||
|
button::Status::Hovered | button::Status::Pressed => hov,
|
||||||
|
_ => base,
|
||||||
|
})),
|
||||||
|
text_color: TEXT_COL,
|
||||||
|
border: Border { color: BORDER_COL, width: 1.0, radius: 4.0.into() },
|
||||||
|
shadow: iced::Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
})
|
||||||
|
.padding([5, 14])
|
||||||
|
};
|
||||||
|
|
||||||
|
let sel_static = SAVE_FORMAT_OPTIONS.iter().copied().find(|&s| s == format);
|
||||||
|
|
||||||
|
let label = |s: &'static str| text(s).size(12).color(Color { r: 0.65, g: 0.65, b: 0.68, a: 1.0 });
|
||||||
|
|
||||||
|
container(
|
||||||
|
column![
|
||||||
|
label("File name"),
|
||||||
|
iced::widget::Space::new().height(4),
|
||||||
|
iced::widget::text_input("drawing.dwg", filename)
|
||||||
|
.on_input(Message::SaveDialogFilenameChanged)
|
||||||
|
.style(input_style)
|
||||||
|
.size(13)
|
||||||
|
.padding([6, 8]),
|
||||||
|
iced::widget::Space::new().height(10),
|
||||||
|
label("Save in"),
|
||||||
|
iced::widget::Space::new().height(4),
|
||||||
|
row![
|
||||||
|
iced::widget::text_input("/home/...", folder)
|
||||||
|
.on_input(Message::SaveDialogFolderChanged)
|
||||||
|
.style(input_style)
|
||||||
|
.size(13)
|
||||||
|
.padding([6, 8])
|
||||||
|
.width(Fill),
|
||||||
|
iced::widget::Space::new().width(6),
|
||||||
|
btn("Browse…", Message::SaveDialogBrowse, BTN_DISC, BTN_DHOV),
|
||||||
|
],
|
||||||
|
iced::widget::Space::new().height(10),
|
||||||
|
label("Format"),
|
||||||
|
iced::widget::Space::new().height(4),
|
||||||
|
pick_list(
|
||||||
|
SAVE_FORMAT_OPTIONS,
|
||||||
|
sel_static,
|
||||||
|
|s: &str| Message::SaveDialogFormatChanged(s.to_string()),
|
||||||
|
)
|
||||||
|
.width(Fill),
|
||||||
|
iced::widget::Space::new().height(16),
|
||||||
|
row![
|
||||||
|
iced::widget::Space::new().width(Fill),
|
||||||
|
btn("Save", Message::SaveDialogConfirm, BTN_OK, BTN_HOV),
|
||||||
|
iced::widget::Space::new().width(8),
|
||||||
|
btn("Cancel", Message::SaveDialogCancel, BTN_DISC, BTN_DHOV),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
.spacing(0),
|
||||||
|
)
|
||||||
|
.style(move |_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(BG)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.padding([20, 24])
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
|
fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
|
||||||
const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
|
const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
|
||||||
const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 };
|
const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 };
|
||||||
|
|
|
||||||
|
|
@ -78,39 +78,33 @@ pub fn load_file(path: &Path) -> Result<CadDocument, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Save dialog ───────────────────────────────────────────────────────────
|
// ── Folder picker (used by the custom Save As dialog) ────────────────────
|
||||||
|
|
||||||
/// Show a save-file dialog listing all DWG and DXF version filters.
|
pub async fn pick_folder(start: &str) -> Option<PathBuf> {
|
||||||
/// DWG versions appear first; format is auto-detected from the returned extension.
|
let mut dlg = rfd::AsyncFileDialog::new().set_title("Choose folder");
|
||||||
pub async fn pick_save_path() -> Option<PathBuf> {
|
if !start.is_empty() {
|
||||||
let dwg_filters: &[(&str, &[&str])] = &[
|
dlg = dlg.set_directory(start);
|
||||||
("DWG Files (2018)", &["dwg"]),
|
|
||||||
("DWG Files (2013)", &["dwg"]),
|
|
||||||
("DWG Files (2010)", &["dwg"]),
|
|
||||||
("DWG Files (2007)", &["dwg"]),
|
|
||||||
("DWG Files (2004)", &["dwg"]),
|
|
||||||
("DWG Files (2000)", &["dwg"]),
|
|
||||||
("DWG Files (R14)", &["dwg"]),
|
|
||||||
("DWG Files (R13)", &["dwg"]),
|
|
||||||
];
|
|
||||||
let dxf_filters: &[(&str, &[&str])] = &[
|
|
||||||
("DXF Files (2018)", &["dxf"]),
|
|
||||||
("DXF Files (2013)", &["dxf"]),
|
|
||||||
("DXF Files (2010)", &["dxf"]),
|
|
||||||
("DXF Files (2007)", &["dxf"]),
|
|
||||||
("DXF Files (2004)", &["dxf"]),
|
|
||||||
("DXF Files (2000)", &["dxf"]),
|
|
||||||
("DXF Files (R14)", &["dxf"]),
|
|
||||||
("DXF Files (R13)", &["dxf"]),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut dlg = rfd::AsyncFileDialog::new()
|
|
||||||
.set_title("Save As")
|
|
||||||
.set_file_name("drawing.dwg");
|
|
||||||
for (label, exts) in dwg_filters.iter().chain(dxf_filters.iter()) {
|
|
||||||
dlg = dlg.add_filter(*label, *exts);
|
|
||||||
}
|
}
|
||||||
dlg.save_file().await.map(|h| h.path().to_path_buf())
|
dlg.pick_folder().await.map(|h| h.path().to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Parse a format string like "DWG 2013" or "DXF 2007" into
|
||||||
|
/// `(extension, DxfVersion)`. Falls back to ("dwg", AC1032) for unknown strings.
|
||||||
|
pub fn parse_save_format(format: &str) -> (&'static str, acadrust::DxfVersion) {
|
||||||
|
use acadrust::DxfVersion;
|
||||||
|
let f = format.to_ascii_uppercase();
|
||||||
|
let is_dxf = f.starts_with("DXF");
|
||||||
|
let ext = if is_dxf { "dxf" } else { "dwg" };
|
||||||
|
let version = if f.contains("2013") { DxfVersion::AC1027 }
|
||||||
|
else if f.contains("2010") { DxfVersion::AC1024 }
|
||||||
|
else if f.contains("2007") { DxfVersion::AC1021 }
|
||||||
|
else if f.contains("2004") { DxfVersion::AC1018 }
|
||||||
|
else if f.contains("2000") { DxfVersion::AC1015 }
|
||||||
|
else if f.contains("R14") { DxfVersion::AC1014 }
|
||||||
|
else { DxfVersion::AC1032 }; // 2018
|
||||||
|
(ext, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Plot Style Table ──────────────────────────────────────────────────────
|
// ── Plot Style Table ──────────────────────────────────────────────────────
|
||||||
|
|
@ -148,21 +142,30 @@ pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
|
||||||
Ok((path, w, h))
|
Ok((path, w, h))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Save ──────────────────────────────────────────────────────────────────
|
/// Save `doc` to `path` with the given DXF version, overriding `doc.version`.
|
||||||
|
|
||||||
/// Save the document to the given path.
|
|
||||||
/// Format is auto-detected from the extension (dwg / dxf).
|
/// Format is auto-detected from the extension (dwg / dxf).
|
||||||
pub fn save(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
pub fn save_as_version(
|
||||||
|
doc: &CadDocument,
|
||||||
|
path: &Path,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut doc = doc.clone();
|
||||||
|
doc.version = version;
|
||||||
let ext = path
|
let ext = path
|
||||||
.extension()
|
.extension()
|
||||||
.map(|e| e.to_string_lossy().to_lowercase())
|
.map(|e| e.to_string_lossy().to_lowercase())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
match ext.as_str() {
|
match ext.as_str() {
|
||||||
"dxf" => save_dxf(doc, path),
|
"dxf" => DxfWriter::new(&doc).write_to_file(path).map_err(|e| e.to_string()),
|
||||||
_ => save_dwg(doc, path),
|
_ => DwgWriter::write_to_file(path, &doc).map_err(|e| e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Save using the document's existing version.
|
||||||
|
pub fn save(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
||||||
|
save_as_version(doc, path, doc.version)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save_dwg(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
pub fn save_dwg(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
||||||
DwgWriter::write_to_file(path, doc).map_err(|e| e.to_string())
|
DwgWriter::write_to_file(path, doc).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue