Guard and preserve behavior forms across document lifecycle

This commit is contained in:
2026-09-06 02:35:28 +03:00
parent 53cd096a56
commit e5660b8902
3 changed files with 495 additions and 10 deletions
+86 -6
View File
@@ -12,6 +12,9 @@ use oa_core::{
use serde_json::{Value, json};
use std::{collections::BTreeSet, path::PathBuf, sync::Arc};
#[cfg(test)]
mod behavior_lifecycle_tests;
#[derive(Debug, Clone)]
pub enum LoadedSnapshot {
Source(DiskProject),
@@ -351,7 +354,10 @@ impl App {
self.draft.as_ref().is_some_and(Draft::dirty)
}
pub fn dirty(&self) -> bool {
self.document.dirty() || self.draft_dirty() || self.source_dirty()
self.document.dirty()
|| self.draft_dirty()
|| self.source_dirty()
|| self.behavior.has_pending()
}
fn sync_source(&mut self) {
self.source_workspace.accept(self.document.bundle());
@@ -435,6 +441,11 @@ impl App {
}
}
pub fn apply_source(&mut self) -> bool {
if self.behavior.has_pending() {
self.status =
"Apply or cancel the behavior form before applying source changes.".into();
return false;
}
if !self.source_dirty() {
return true;
}
@@ -465,6 +476,10 @@ impl App {
}
}
pub fn apply(&mut self) -> bool {
if self.behavior.has_pending() {
self.status = "Apply or cancel the behavior form before changing the model.".into();
return false;
}
if self.source_dirty() {
self.status =
"Apply all source drafts or revert them before changing the model.".into();
@@ -540,6 +555,30 @@ impl App {
}
}
}
fn apply_behavior(&mut self) -> bool {
if !self.behavior.has_pending() {
return true;
}
if self.source_dirty() || self.draft_dirty() {
self.status = "Property, source and behavior drafts are retained. Revert the other drafts before applying the behavior form.".into();
return false;
}
match self.behavior.prepare_pending(self.project()) {
Ok(Some(command)) => {
if self.execute(command) {
self.behavior.acknowledge_commit();
true
} else {
false
}
}
Ok(None) => true,
Err(error) => {
self.status = error;
false
}
}
}
fn reset(&mut self, project: Project, path: Option<PathBuf>) {
let window_size = self.window_size;
let canvas_size = self.canvas_size;
@@ -674,7 +713,7 @@ impl App {
) {
self.model_menu = None;
}
if self.source_dirty()
if (self.source_dirty() || self.behavior.has_pending())
&& matches!(
message,
Message::Field(..)
@@ -687,7 +726,8 @@ impl App {
)
{
self.status =
"Apply all source drafts or revert them before editing properties.".into();
"Apply or cancel pending source and behavior drafts before editing properties."
.into();
return Task::none();
}
if self.busy
@@ -768,6 +808,35 @@ impl App {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
self.selected_bend = None;
}
if self.behavior.has_pending()
&& matches!(
message,
Message::Page(page) if page != Page::Behavior
)
{
self.status = "Apply or cancel the behavior form before leaving this view.".into();
return Task::none();
}
if self.behavior.has_pending()
&& matches!(
message,
Message::BrowseGroup(_)
| Message::SourceEdit(_)
| Message::SourceCreateFile
| Message::SourceCloseFile(_)
| Message::SourceUndo
| Message::SourceRedo
| Message::SourceRecover
| Message::SourceLoadIncludes
| Message::Menu(_)
| Message::Connection(_)
| Message::PortAnchor(_)
| Message::Route(_)
)
{
self.status = "Apply or cancel the behavior form before changing another view.".into();
return Task::none();
}
let previous = self.selected;
let previous_layer = self.layer;
let entering_catalog =
@@ -1297,14 +1366,22 @@ impl App {
}
Message::InspectorTab(tab) => self.inspector_tab = tab,
Message::Behavior(event) => {
if event.is_submit() && !self.apply() {
if event.starts_form() && !self.behavior.has_pending() && !self.apply() {
return Task::none();
}
if event.is_submit() && (self.source_dirty() || self.draft_dirty()) {
self.status = "Revert the other drafts before applying the behavior form. Your inputs are retained.".into();
return Task::none();
}
let mut state = std::mem::take(&mut self.behavior);
let result = state.update(self.project(), event);
self.behavior = state;
match result {
Ok(Some(command)) => return self.handle(Message::Command(command)),
Ok(Some(command)) => {
if self.execute(command) {
self.behavior.acknowledge_commit();
}
}
Ok(None) => {}
Err(error) => self.status = error,
}
@@ -1440,6 +1517,9 @@ impl App {
}
}
Message::ConfirmDelete => {
if !self.delete_pending || self.behavior.has_pending() {
return Task::none();
}
self.delete_pending = false;
self.execute(Command::Delete(self.selected));
}
@@ -1858,7 +1938,7 @@ impl App {
return self.handle(Message::Save(false));
}
Message::Save(save_as) => {
if !self.apply_source() || !self.apply() {
if !self.apply_behavior() || !self.apply_source() || !self.apply() {
self.continue_after_save = false;
self.pending = None;
return Task::none();