forked from 1b-agent/opencadstudio-upstream
fix(io): guard concurrent drawing saves
Hold edit leases and compare disk fingerprints before replacement so another editor cannot be silently overwritten. Offer retry, reload, Save As, or explicit overwrite recovery. Closes #498
This commit is contained in:
1 parent
9d973a0fd6
commit
2acc4f7978
12 files changed
+1424
-23
No files matched your search
@@ -272,6 +272,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
crate::app::SavePurpose::Manual,
|
||||
continuation,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return Task::batch([close, save]);
|
||||
}
|
||||
|
||||
+541
-3
@@ -43,6 +43,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn native_paths_match(left: &std::path::Path, right: &std::path::Path) -> bool {
|
||||
match (
|
||||
std::fs::canonicalize(left),
|
||||
std::fs::canonicalize(right),
|
||||
) {
|
||||
(Ok(left), Ok(right)) => left == right,
|
||||
_ => left == right,
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Before a save, give every cached truck solid that still has no ACIS
|
||||
/// geometry (EXTRUDE/REVOLVE/SWEEP/LOFT/boolean results) an exact modeler
|
||||
@@ -390,7 +401,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.opening = Some(crate::app::OpenProgress {
|
||||
name: "Opening…".into(),
|
||||
size_bytes: 0,
|
||||
state: state.clone(),
|
||||
state: state.clone(),
|
||||
started: Instant::now(),
|
||||
});
|
||||
Task::perform(crate::io::pick_and_load_web(state), Message::FileOpened)
|
||||
@@ -426,6 +437,233 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn install_native_edit_guard(
|
||||
&mut self,
|
||||
i: usize,
|
||||
path: &std::path::Path,
|
||||
loaded_fingerprint: Option<crate::io::edit_lock::FileFingerprint>,
|
||||
) {
|
||||
let current_fingerprint =
|
||||
crate::io::edit_lock::FileFingerprint::capture(path).ok();
|
||||
if loaded_fingerprint
|
||||
.as_ref()
|
||||
.zip(current_fingerprint.as_ref())
|
||||
.is_some_and(|(loaded, current)| loaded != current)
|
||||
{
|
||||
self.command_line.push_error_once(
|
||||
"Drawing changed on disk while it was opening; Save will require conflict resolution.",
|
||||
);
|
||||
}
|
||||
self.tabs[i].disk_fingerprint =
|
||||
loaded_fingerprint.or(current_fingerprint);
|
||||
match crate::io::edit_lock::EditLease::acquire(path) {
|
||||
Ok(lease) => {
|
||||
if let Some(warning) = lease.platform_warning() {
|
||||
self.command_line.push_info(&format!(
|
||||
"Edit lease active; {warning}. External-change checks remain active."
|
||||
));
|
||||
}
|
||||
self.tabs[i].edit_lease = Some(lease);
|
||||
self.tabs[i].edit_lock_conflict = false;
|
||||
}
|
||||
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => {
|
||||
self.tabs[i].edit_lease = None;
|
||||
self.tabs[i].edit_lock_conflict = true;
|
||||
self.command_line.push_error_once(&format!(
|
||||
"Opened read-only against other editors: {error}"
|
||||
));
|
||||
}
|
||||
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
||||
self.tabs[i].edit_lease = None;
|
||||
self.tabs[i].edit_lock_conflict = false;
|
||||
self.command_line.push_info(&format!(
|
||||
"{error}. External-change checks remain active."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(in crate::app) fn refresh_native_edit_guard_after_save(
|
||||
&mut self,
|
||||
i: usize,
|
||||
path: &std::path::Path,
|
||||
path_changed: bool,
|
||||
destination_lease: Option<crate::io::edit_lock::EditLease>,
|
||||
) {
|
||||
if path_changed {
|
||||
self.tabs[i].edit_lease = None;
|
||||
self.tabs[i].edit_lease = destination_lease;
|
||||
self.tabs[i].edit_lock_conflict = false;
|
||||
if self.tabs[i].edit_lease.is_none() {
|
||||
self.install_native_edit_guard(i, path, None);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let refresh = self.tabs[i].edit_lease.as_mut().map(|lease| {
|
||||
lease
|
||||
.refresh_drawing_lock(path)
|
||||
.map(|_| lease.platform_warning().map(str::to_owned))
|
||||
});
|
||||
match refresh {
|
||||
Some(Ok(warning)) => {
|
||||
self.tabs[i].edit_lock_conflict = false;
|
||||
if let Some(warning) = warning {
|
||||
self.command_line.push_info(&format!(
|
||||
"{warning}. External-change checks remain active."
|
||||
));
|
||||
}
|
||||
}
|
||||
Some(Err(crate::io::edit_lock::EditLeaseError::Locked(error))) => {
|
||||
self.tabs[i].edit_lock_conflict = true;
|
||||
self.command_line.push_error_once(&format!(
|
||||
"Saved, but the refreshed drawing is locked by another editor: {error}"
|
||||
));
|
||||
}
|
||||
Some(Err(crate::io::edit_lock::EditLeaseError::Unavailable(error))) => {
|
||||
self.tabs[i].edit_lock_conflict = false;
|
||||
self.command_line.push_info(&format!(
|
||||
"{error}. External-change checks remain active."
|
||||
));
|
||||
}
|
||||
None => self.install_native_edit_guard(i, path, None),
|
||||
}
|
||||
self.tabs[i].disk_fingerprint =
|
||||
crate::io::edit_lock::FileFingerprint::capture(path).ok();
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(in crate::app) fn save_tab_synchronously_protected(
|
||||
&mut self,
|
||||
i: usize,
|
||||
path: std::path::PathBuf,
|
||||
set_current_path: bool,
|
||||
) -> Result<(), crate::io::SaveFailure> {
|
||||
let path_changed = self.tabs[i]
|
||||
.current_path
|
||||
.as_deref()
|
||||
.is_none_or(|current| !native_paths_match(current, &path));
|
||||
if !path_changed && self.tabs[i].edit_lock_conflict {
|
||||
return Err(crate::io::SaveFailure::file_in_use(
|
||||
"drawing edit lock is held by another editor",
|
||||
));
|
||||
}
|
||||
|
||||
let destination_lease = if path_changed {
|
||||
match crate::io::edit_lock::EditLease::acquire(&path) {
|
||||
Ok(lease) => Some(lease),
|
||||
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => {
|
||||
return Err(crate::io::SaveFailure::file_in_use(error));
|
||||
}
|
||||
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
||||
self.command_line.push_info(&format!(
|
||||
"{error}. External-change checks remain active."
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let expected_fingerprint = if path_changed {
|
||||
match crate::io::edit_lock::FileFingerprint::capture(&path) {
|
||||
Ok(fingerprint) => Some(fingerprint),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(error) => {
|
||||
return Err(crate::io::SaveFailure::other(format!(
|
||||
"could not verify {} before saving: {error}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.tabs[i].disk_fingerprint.clone().or_else(|| {
|
||||
crate::io::edit_lock::FileFingerprint::capture(&path).ok()
|
||||
})
|
||||
};
|
||||
|
||||
self.prepare_native_save(i);
|
||||
let version = self.tabs[i].scene.document.version;
|
||||
let snapshot = self.tabs[i].scene.document.clone();
|
||||
crate::io::save_owned_as_version_atomic(
|
||||
snapshot,
|
||||
&path,
|
||||
version,
|
||||
self.backup_on_save,
|
||||
expected_fingerprint,
|
||||
)?;
|
||||
|
||||
if set_current_path {
|
||||
self.tabs[i].current_path = Some(path.clone());
|
||||
}
|
||||
self.refresh_native_edit_guard_after_save(
|
||||
i,
|
||||
&path,
|
||||
path_changed,
|
||||
destination_lease,
|
||||
);
|
||||
self.tabs[i].dirty = false;
|
||||
let _ = std::fs::remove_file(path.with_extension("sv$"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn retry_native_edit_guard(
|
||||
&mut self,
|
||||
i: usize,
|
||||
path: &std::path::Path,
|
||||
) -> Result<(), String> {
|
||||
if let Some(lease) = self.tabs[i].edit_lease.as_mut() {
|
||||
match lease.refresh_drawing_lock(path) {
|
||||
Ok(()) => {
|
||||
let warning = lease.platform_warning().map(str::to_owned);
|
||||
self.tabs[i].edit_lock_conflict = false;
|
||||
if let Some(warning) = warning {
|
||||
self.command_line.push_info(&format!(
|
||||
"{warning}. External-change checks remain active."
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => {
|
||||
return Err(error);
|
||||
}
|
||||
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
||||
self.tabs[i].edit_lock_conflict = false;
|
||||
self.command_line.push_info(&format!(
|
||||
"{error}. External-change checks remain active."
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match crate::io::edit_lock::EditLease::acquire(path) {
|
||||
Ok(lease) => {
|
||||
let warning = lease.platform_warning().map(str::to_owned);
|
||||
self.tabs[i].edit_lease = Some(lease);
|
||||
self.tabs[i].edit_lock_conflict = false;
|
||||
if let Some(warning) = warning {
|
||||
self.command_line.push_info(&format!(
|
||||
"{warning}. External-change checks remain active."
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => Err(error),
|
||||
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
||||
self.tabs[i].edit_lock_conflict = false;
|
||||
self.command_line.push_info(&format!(
|
||||
"{error}. External-change checks remain active."
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_file_opened(&mut self, name: String, path: std::path::PathBuf, doc: acadrust::CadDocument,
|
||||
mut caches: crate::scene::DerivedCaches,
|
||||
) -> Task<Message> {
|
||||
@@ -490,7 +728,14 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
idx
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let opened_fingerprint = self
|
||||
.opening
|
||||
.as_ref()
|
||||
.and_then(|opening| opening.fingerprint.clone());
|
||||
self.tabs[i].current_path = Some(path.clone());
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.install_native_edit_guard(i, &path, opened_fingerprint);
|
||||
self.tabs[i].scene.material_base_dir =
|
||||
path.parent().map(std::path::Path::to_path_buf);
|
||||
self.tabs[i].scene.document = doc;
|
||||
@@ -749,6 +994,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
purpose: crate::app::SavePurpose,
|
||||
continuation: crate::app::SaveContinuation,
|
||||
set_current_path: bool,
|
||||
check_external_change: bool,
|
||||
) -> Task<Message> {
|
||||
let tab_id = self.tabs[i].id;
|
||||
if self.active_save_jobs.contains_key(&tab_id) {
|
||||
@@ -758,6 +1004,67 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
if purpose != crate::app::SavePurpose::Autosave
|
||||
&& !set_current_path
|
||||
&& self.tabs[i].edit_lock_conflict
|
||||
{
|
||||
let error = "Drawing edit lock is held by another editor.".to_string();
|
||||
self.command_line.push_error_once(&format!(
|
||||
"Unable to save \"{}\": {error}",
|
||||
path.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| path.display().to_string())
|
||||
));
|
||||
self.pending_save_failure = Some(crate::app::PendingSaveFailure {
|
||||
tab_id,
|
||||
path,
|
||||
version,
|
||||
purpose,
|
||||
continuation,
|
||||
set_current_path,
|
||||
error,
|
||||
});
|
||||
self.restore_failed_save_continuation(continuation, i);
|
||||
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let destination_is_current = self.tabs[i]
|
||||
.current_path
|
||||
.as_deref()
|
||||
.is_some_and(|current| native_paths_match(current, &path));
|
||||
if set_current_path && !destination_is_current {
|
||||
match crate::io::edit_lock::EditLease::acquire(&path) {
|
||||
Ok(lease) => {
|
||||
self.pending_save_leases.insert(tab_id, lease);
|
||||
}
|
||||
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => {
|
||||
self.command_line.push_error_once(&format!(
|
||||
"Unable to save \"{}\": {error}",
|
||||
path.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| path.display().to_string())
|
||||
));
|
||||
self.pending_save_failure = Some(crate::app::PendingSaveFailure {
|
||||
tab_id,
|
||||
path,
|
||||
version,
|
||||
purpose,
|
||||
continuation,
|
||||
set_current_path,
|
||||
error,
|
||||
});
|
||||
self.restore_failed_save_continuation(continuation, i);
|
||||
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
||||
return Task::none();
|
||||
}
|
||||
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
||||
self.command_line.push_info(&format!(
|
||||
"{error}. External-change checks remain active."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let epoch = self.tabs[i].scene.geometry_epoch;
|
||||
let revision = self.tabs[i].edit_revision;
|
||||
@@ -803,6 +1110,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.active_save_jobs.insert(tab_id, job_id);
|
||||
let previous_autosave = set_current_path.then(|| self.autosave_target(i));
|
||||
let backup = purpose != crate::app::SavePurpose::Autosave && self.backup_on_save;
|
||||
let expected_fingerprint =
|
||||
if check_external_change && purpose != crate::app::SavePurpose::Autosave {
|
||||
if set_current_path {
|
||||
crate::io::edit_lock::FileFingerprint::capture(&path).ok()
|
||||
} else {
|
||||
self.tabs[i].disk_fingerprint.clone()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let worker_path = path.clone();
|
||||
|
||||
Task::perform(
|
||||
@@ -832,11 +1149,17 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
&worker_path,
|
||||
version,
|
||||
backup,
|
||||
expected_fingerprint,
|
||||
);
|
||||
(result, refreshed_preview)
|
||||
})
|
||||
.join()
|
||||
.unwrap_or_else(|_| (Err("save worker panicked".to_string()), None));
|
||||
.unwrap_or_else(|_| {
|
||||
(
|
||||
Err(crate::io::SaveFailure::other("save worker panicked")),
|
||||
None,
|
||||
)
|
||||
});
|
||||
crate::app::SaveOutcome {
|
||||
job_id,
|
||||
tab_id,
|
||||
@@ -844,6 +1167,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
revision,
|
||||
camera_generation,
|
||||
path,
|
||||
version,
|
||||
previous_autosave,
|
||||
set_current_path,
|
||||
purpose,
|
||||
@@ -867,6 +1191,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
if latest {
|
||||
self.active_save_jobs.remove(&outcome.tab_id);
|
||||
}
|
||||
let destination_lease = self.pending_save_leases.remove(&outcome.tab_id);
|
||||
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == outcome.tab_id) else {
|
||||
if outcome.purpose == crate::app::SavePurpose::Autosave {
|
||||
@@ -881,7 +1206,60 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if let Err(error) = outcome.result {
|
||||
if let Err(error) = &outcome.result {
|
||||
if error.externally_modified
|
||||
&& outcome.purpose != crate::app::SavePurpose::Autosave
|
||||
{
|
||||
let file_name = outcome
|
||||
.path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| outcome.path.display().to_string());
|
||||
self.command_line.push_error_once(&format!(
|
||||
"Save stopped: \"{file_name}\" changed outside Open CAD Studio."
|
||||
));
|
||||
self.pending_external_change = Some(crate::app::PendingExternalChange {
|
||||
tab_id: outcome.tab_id,
|
||||
path: outcome.path.clone(),
|
||||
version: outcome.version,
|
||||
purpose: outcome.purpose,
|
||||
continuation: outcome.continuation,
|
||||
set_current_path: outcome.set_current_path,
|
||||
});
|
||||
self.restore_failed_save_continuation(outcome.continuation, i);
|
||||
self.active_modal = Some(crate::app::ModalKind::ExternalChange);
|
||||
return Task::none();
|
||||
}
|
||||
if error.file_in_use && outcome.purpose != crate::app::SavePurpose::Autosave {
|
||||
let file_name = outcome
|
||||
.path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| outcome.path.display().to_string());
|
||||
self.command_line.push_error_once(&format!(
|
||||
"Unable to save \"{file_name}\": file is in use by another application."
|
||||
));
|
||||
self.pending_save_failure = Some(crate::app::PendingSaveFailure {
|
||||
tab_id: outcome.tab_id,
|
||||
path: outcome.path.clone(),
|
||||
version: outcome.version,
|
||||
purpose: outcome.purpose,
|
||||
continuation: outcome.continuation,
|
||||
set_current_path: outcome.set_current_path,
|
||||
error: error.to_string(),
|
||||
});
|
||||
match outcome.continuation {
|
||||
crate::app::SaveContinuation::CloseTab => {
|
||||
self.pending_close = Some(crate::app::PendingClose::Tab(i));
|
||||
}
|
||||
crate::app::SaveContinuation::Quit => {
|
||||
self.pending_close = Some(crate::app::PendingClose::Quit);
|
||||
}
|
||||
crate::app::SaveContinuation::None => {}
|
||||
}
|
||||
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
||||
return Task::none();
|
||||
}
|
||||
self.command_line
|
||||
.push_error(&format!("Save failed: {error}"));
|
||||
return match outcome.continuation {
|
||||
@@ -912,6 +1290,13 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.command_line.push_output("Autosaved 1 drawing");
|
||||
}
|
||||
crate::app::SavePurpose::Manual | crate::app::SavePurpose::SaveAs => {
|
||||
let path_changed = outcome.set_current_path
|
||||
&& self.tabs[i]
|
||||
.current_path
|
||||
.as_deref()
|
||||
.is_none_or(|current| {
|
||||
!native_paths_match(current, &outcome.path)
|
||||
});
|
||||
self.command_line
|
||||
.push_output(&format!("Saved: {}", outcome.path.display()));
|
||||
self.recent_thumbs.remove(&outcome.path);
|
||||
@@ -924,6 +1309,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.tabs[i].current_path = Some(outcome.path.clone());
|
||||
tasks.push(self.push_recent(outcome.path.clone()));
|
||||
}
|
||||
self.refresh_native_edit_guard_after_save(
|
||||
i,
|
||||
&outcome.path,
|
||||
path_changed,
|
||||
destination_lease,
|
||||
);
|
||||
if snapshot_is_current {
|
||||
self.tabs[i].dirty = false;
|
||||
let _ = std::fs::remove_file(outcome.path.with_extension("sv$"));
|
||||
@@ -961,6 +1352,150 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn restore_failed_save_continuation(
|
||||
&mut self,
|
||||
continuation: crate::app::SaveContinuation,
|
||||
tab_idx: usize,
|
||||
) {
|
||||
self.pending_close = match continuation {
|
||||
crate::app::SaveContinuation::None => None,
|
||||
crate::app::SaveContinuation::CloseTab => {
|
||||
Some(crate::app::PendingClose::Tab(tab_idx))
|
||||
}
|
||||
crate::app::SaveContinuation::Quit => Some(crate::app::PendingClose::Quit),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn on_save_file_in_use_retry(&mut self) -> Task<Message> {
|
||||
let Some(mut failure) = self.pending_save_failure.take() else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == failure.tab_id) else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
if self.tabs[i].edit_lock_conflict {
|
||||
if let Err(error) = self.retry_native_edit_guard(i, &failure.path) {
|
||||
let continuation = failure.continuation;
|
||||
failure.error = error.clone();
|
||||
self.pending_save_failure = Some(failure);
|
||||
self.restore_failed_save_continuation(continuation, i);
|
||||
self.command_line
|
||||
.push_error_once(&format!("Unable to acquire edit lock: {error}"));
|
||||
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
self.close_active_modal();
|
||||
self.restore_failed_save_continuation(failure.continuation, i);
|
||||
self.active_tab = i;
|
||||
self.prepare_native_save(i);
|
||||
self.queue_native_save(
|
||||
i,
|
||||
failure.path,
|
||||
failure.version,
|
||||
failure.purpose,
|
||||
failure.continuation,
|
||||
failure.set_current_path,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn on_save_file_in_use_save_as(&mut self) -> Task<Message> {
|
||||
let Some(failure) = self.pending_save_failure.take() else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == failure.tab_id) else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
self.close_active_modal();
|
||||
self.restore_failed_save_continuation(failure.continuation, i);
|
||||
self.active_tab = i;
|
||||
self.save_dialog_for_unsaved =
|
||||
failure.continuation != crate::app::SaveContinuation::None;
|
||||
self.open_save_dialog_window(i)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn on_external_change_overwrite(&mut self) -> Task<Message> {
|
||||
let Some(conflict) = self.pending_external_change.take() else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == conflict.tab_id) else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
self.close_active_modal();
|
||||
self.restore_failed_save_continuation(conflict.continuation, i);
|
||||
self.active_tab = i;
|
||||
self.prepare_native_save(i);
|
||||
self.queue_native_save(
|
||||
i,
|
||||
conflict.path,
|
||||
conflict.version,
|
||||
conflict.purpose,
|
||||
conflict.continuation,
|
||||
conflict.set_current_path,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn on_external_change_save_as(&mut self) -> Task<Message> {
|
||||
let Some(conflict) = self.pending_external_change.take() else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == conflict.tab_id) else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
self.close_active_modal();
|
||||
self.restore_failed_save_continuation(conflict.continuation, i);
|
||||
self.active_tab = i;
|
||||
self.save_dialog_for_unsaved =
|
||||
conflict.continuation != crate::app::SaveContinuation::None;
|
||||
self.open_save_dialog_window(i)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn on_external_change_reload(&mut self) -> Task<Message> {
|
||||
let Some(conflict) = self.pending_external_change.take() else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == conflict.tab_id) else {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
let metadata = match std::fs::metadata(&conflict.path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) => {
|
||||
self.close_active_modal();
|
||||
self.command_line
|
||||
.push_error(&format!("Reload failed: {error}"));
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
self.close_active_modal();
|
||||
self.pending_close = None;
|
||||
self.tab_counter += 1;
|
||||
self.tabs[i] = crate::app::document::DocumentTab::new_drawing(self.tab_counter);
|
||||
self.active_tab = i;
|
||||
self.apply_bg_default(i);
|
||||
self.update(Message::OpenPathPicked(Some((
|
||||
conflict.path,
|
||||
metadata.len(),
|
||||
))))
|
||||
}
|
||||
|
||||
pub(super) fn on_save_file(&mut self) -> Task<Message> {
|
||||
if self.read_only {
|
||||
self.command_line
|
||||
@@ -989,6 +1524,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
crate::app::SavePurpose::Manual,
|
||||
crate::app::SaveContinuation::None,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
}
|
||||
self.save_dialog_for_unsaved = false;
|
||||
@@ -1151,6 +1687,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
crate::app::SavePurpose::SaveAs,
|
||||
continuation,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1219,6 +1756,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
crate::app::SavePurpose::Autosave,
|
||||
crate::app::SaveContinuation::None,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
}
|
||||
Task::batch(tasks)
|
||||
|
||||
@@ -80,6 +80,16 @@ impl OpenCADStudio {
|
||||
if self.active_modal == Some(ScaleManager) {
|
||||
self.scale_stage_discard();
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.active_modal == Some(FileInUse) {
|
||||
self.pending_save_failure = None;
|
||||
self.pending_close = None;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.active_modal == Some(ExternalChange) {
|
||||
self.pending_external_change = None;
|
||||
self.pending_close = None;
|
||||
}
|
||||
match self.active_modal {
|
||||
// Dismissing these via ✕ is the cancel/decline path.
|
||||
Some(Unsaved) => self.pending_close = None,
|
||||
@@ -457,6 +467,9 @@ impl OpenCADStudio {
|
||||
size_bytes,
|
||||
state: progress.clone(),
|
||||
started: Instant::now(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fingerprint:
|
||||
crate::io::edit_lock::FileFingerprint::capture(&path).ok(),
|
||||
});
|
||||
let size_label = format_size(size_bytes);
|
||||
self.command_line
|
||||
@@ -4067,6 +4080,33 @@ impl OpenCADStudio {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Message::SaveFinished(outcome) => self.on_save_finished(outcome),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Message::SaveFileInUseRetry => self.on_save_file_in_use_retry(),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Message::SaveFileInUseSaveAs => self.on_save_file_in_use_save_as(),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Message::SaveFileInUseCancel => {
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Message::ExternalChangeReload => self.on_external_change_reload(),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Message::ExternalChangeSaveAs => self.on_external_change_save_as(),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Message::ExternalChangeOverwrite => self.on_external_change_overwrite(),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Message::ExternalChangeCancel => {
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// ── Page Setup ────────────────────────────────────────────────
|
||||
Message::UpdateCheckResult(latest) => {
|
||||
let Some(info) = latest else {
|
||||
|
||||
Reference in new issue
Block a user