hydrate_model/editor/
editor_model.rs

1use crate::edit_context::EditContext;
2use crate::editor::undo::UndoStack;
3use crate::{
4    AssetId, AssetPath, AssetPathCache, AssetSourceId, DataSet, DataSource,
5    FileSystemIdBasedDataSource, FileSystemPathBasedDataSource, HashMap, PathNode, PathNodeRoot,
6    PendingFileOperations, SchemaNamedType, SchemaSet,
7};
8use hydrate_data::{
9    AssetLocation, AssetName, CanonicalPathReference, DataSetError, DataSetResult, ImportInfo,
10    PathReferenceHash, SingleObject,
11};
12use hydrate_pipeline::{
13    DynEditorModel, HydrateProjectConfiguration, ImportJobToQueue, ImporterRegistry,
14};
15use hydrate_schema::{SchemaFingerprint, SchemaRecord};
16use slotmap::DenseSlotMap;
17use std::path::PathBuf;
18slotmap::new_key_type! { pub struct EditContextKey; }
19
20pub struct EditorModel {
21    project_config: HydrateProjectConfiguration,
22    schema_set: SchemaSet,
23    undo_stack: UndoStack,
24    root_edit_context_key: EditContextKey,
25    edit_contexts: DenseSlotMap<EditContextKey, EditContext>,
26    //TODO: slot_map?
27    data_sources: HashMap<AssetSourceId, Box<dyn DataSource>>,
28
29    path_node_schema: SchemaNamedType,
30    path_node_root_schema: SchemaNamedType,
31}
32
33pub struct EditorModelWithCache<'a> {
34    pub asset_path_cache: &'a AssetPathCache,
35    pub editor_model: &'a mut EditorModel,
36}
37
38impl<'a> DynEditorModel for EditorModelWithCache<'a> {
39    fn schema_set(&self) -> &SchemaSet {
40        self.editor_model.schema_set()
41    }
42
43    fn handle_import_complete(
44        &mut self,
45        asset_id: AssetId,
46        asset_name: AssetName,
47        asset_location: AssetLocation,
48        default_asset: &SingleObject,
49        replace_with_default_asset: bool,
50        import_info: ImportInfo,
51        canonical_path_references: &HashMap<CanonicalPathReference, AssetId>,
52        _path_references: &HashMap<PathReferenceHash, CanonicalPathReference>,
53    ) -> DataSetResult<()> {
54        //
55        // If the asset is supposed to be regenerated, stomp the existing asset
56        //
57        let edit_context = self.editor_model.root_edit_context_mut();
58        if replace_with_default_asset {
59            if edit_context.has_asset(asset_id) {
60                edit_context.delete_asset(asset_id)?;
61            }
62
63            edit_context.init_from_single_object(
64                asset_id,
65                asset_name,
66                asset_location,
67                default_asset,
68            )?;
69        }
70
71        //
72        // Whether it is regenerated or not, update import data
73        //
74        edit_context.set_import_info(asset_id, import_info)?;
75        for (path_reference, referenced_asset_id) in canonical_path_references {
76            edit_context.set_path_reference_override(
77                asset_id,
78                path_reference.clone(),
79                *referenced_asset_id,
80            )?;
81        }
82
83        Ok(())
84    }
85
86    fn data_set(&self) -> &DataSet {
87        self.editor_model.root_edit_context().data_set()
88    }
89
90    fn is_path_node_or_root(
91        &self,
92        schema_record: &SchemaRecord,
93    ) -> bool {
94        self.editor_model
95            .is_path_node_or_root(schema_record.fingerprint())
96    }
97
98    fn asset_display_name_long(
99        &self,
100        asset_id: AssetId,
101    ) -> String {
102        self.editor_model
103            .asset_display_name_long(asset_id, &self.asset_path_cache)
104    }
105}
106
107impl EditorModel {
108    pub fn new(
109        project_config: HydrateProjectConfiguration,
110        schema_set: SchemaSet,
111    ) -> Self {
112        let undo_stack = UndoStack::default();
113        let mut edit_contexts: DenseSlotMap<EditContextKey, EditContext> = Default::default();
114
115        let root_edit_context_key = edit_contexts.insert_with_key(|key| {
116            EditContext::new(&project_config, key, schema_set.clone(), &undo_stack)
117        });
118
119        let path_node_root_schema = schema_set
120            .find_named_type(PathNodeRoot::schema_name())
121            .unwrap()
122            .clone();
123
124        let path_node_schema = schema_set
125            .find_named_type(PathNode::schema_name())
126            .unwrap()
127            .clone();
128
129        EditorModel {
130            project_config,
131            schema_set,
132            undo_stack,
133            root_edit_context_key,
134            edit_contexts,
135            data_sources: Default::default(),
136            //location_tree: Default::default(),
137            //asset_path_cache: AssetPathCache::empty(),
138            path_node_root_schema,
139            path_node_schema,
140        }
141    }
142
143    pub fn path_node_schema(&self) -> &SchemaNamedType {
144        &self.path_node_schema
145    }
146
147    pub fn path_node_root_schema(&self) -> &SchemaNamedType {
148        &self.path_node_root_schema
149    }
150
151    pub fn data_sources(&self) -> &HashMap<AssetSourceId, Box<dyn DataSource>> {
152        &self.data_sources
153    }
154
155    pub fn is_path_node_or_root(
156        &self,
157        fingerprint: SchemaFingerprint,
158    ) -> bool {
159        self.path_node_schema.fingerprint() == fingerprint
160            || self.path_node_root_schema.fingerprint() == fingerprint
161    }
162
163    pub fn is_generated_asset(
164        &self,
165        asset_id: AssetId,
166    ) -> bool {
167        for data_source in self.data_sources.values() {
168            if data_source.is_generated_asset(asset_id) {
169                return true;
170            }
171        }
172
173        false
174    }
175
176    pub fn persist_generated_asset(
177        &mut self,
178        asset_id: AssetId,
179    ) {
180        for (_, data_source) in &mut self.data_sources {
181            let root_edit_context = self
182                .edit_contexts
183                .get_mut(self.root_edit_context_key)
184                .unwrap();
185
186            data_source.persist_generated_asset(root_edit_context, asset_id);
187        }
188    }
189
190    pub fn commit_all_pending_undo_contexts(&mut self) {
191        for (_, context) in &mut self.edit_contexts {
192            context.commit_pending_undo_context();
193        }
194    }
195
196    pub fn cancel_all_pending_undo_contexts(&mut self) {
197        for (_, context) in &mut self.edit_contexts {
198            context.commit_pending_undo_context();
199        }
200    }
201
202    pub fn any_edit_context_has_unsaved_changes(&self) -> bool {
203        for (_, data_source) in &self.data_sources {
204            for (_, edit_context) in &self.edit_contexts {
205                if data_source.edit_context_has_unsaved_changes(edit_context) {
206                    return true;
207                }
208            }
209        }
210
211        false
212    }
213
214    pub fn pending_file_operations(&self) -> PendingFileOperations {
215        let mut pending_file_operations = PendingFileOperations::default();
216
217        for (_, data_source) in &self.data_sources {
218            for (_, edit_context) in &self.edit_contexts {
219                data_source
220                    .append_pending_file_operations(edit_context, &mut pending_file_operations);
221            }
222        }
223
224        pending_file_operations
225    }
226
227    pub fn schema_set(&self) -> &SchemaSet {
228        &self.schema_set
229    }
230
231    pub fn clone_schema_set(&self) -> SchemaSet {
232        self.schema_set.clone()
233    }
234
235    pub fn root_edit_context(&self) -> &EditContext {
236        self.edit_contexts.get(self.root_edit_context_key).unwrap()
237    }
238
239    pub fn root_edit_context_mut(&mut self) -> &mut EditContext {
240        self.edit_contexts
241            .get_mut(self.root_edit_context_key)
242            .unwrap()
243    }
244
245    pub fn asset_path(
246        &self,
247        asset_id: AssetId,
248        asset_path_cache: &AssetPathCache,
249    ) -> Option<AssetPath> {
250        let root_data_set = &self.root_edit_context().data_set;
251        let location = root_data_set.asset_location(asset_id);
252
253        // Look up the location, if we don't find it just assume the asset is at the root. This
254        // allows some degree of robustness even when data is in a bad state (like cyclical references)
255        let path = location
256            .map(|x| asset_path_cache.path_to_id_lookup().get(&x.path_node_id()))
257            .flatten()
258            .cloned()?;
259
260        let name = root_data_set.asset_name(asset_id).unwrap().as_string();
261        if let Some(name) = name {
262            Some(path.join(name))
263        } else {
264            Some(path.join(&format!("{}", asset_id.as_uuid())))
265        }
266    }
267
268    pub fn asset_display_name_long(
269        &self,
270        asset_id: AssetId,
271        asset_path_cache: &AssetPathCache,
272    ) -> String {
273        self.asset_path(asset_id, asset_path_cache)
274            .map(|x| x.as_str().to_string())
275            .unwrap_or_else(|| format!("{}", asset_id.as_uuid()))
276    }
277
278    pub fn data_source(
279        &mut self,
280        asset_source_id: AssetSourceId,
281    ) -> Option<&dyn DataSource> {
282        self.data_sources.get(&asset_source_id).map(|x| &**x)
283    }
284
285    pub fn is_a_root_asset(
286        &self,
287        asset_id: AssetId,
288    ) -> bool {
289        for source in self.data_sources.keys() {
290            if *source.uuid() == asset_id.as_uuid() {
291                return true;
292            }
293        }
294
295        false
296    }
297
298    pub fn add_file_system_id_based_asset_source<RootPathT: Into<PathBuf>>(
299        &mut self,
300        project_config: &HydrateProjectConfiguration,
301        data_source_name: &str,
302        file_system_root_path: RootPathT,
303        import_job_to_queue: &mut ImportJobToQueue,
304    ) -> AssetSourceId {
305        let file_system_root_path = dunce::canonicalize(&file_system_root_path.into()).unwrap();
306        let path_node_root_schema = self.path_node_root_schema.as_record().unwrap().clone();
307        let root_edit_context = self.root_edit_context_mut();
308
309        // Commit any pending changes so we have a clean change tracking state
310        root_edit_context.commit_pending_undo_context();
311
312        //
313        // Create the PathNodeRoot asset that acts as the root location for all assets in this DS
314        //
315        let asset_source_id = AssetSourceId::new();
316        let root_asset_id = AssetId::from_uuid(*asset_source_id.uuid());
317        root_edit_context
318            .new_asset_with_id(
319                root_asset_id,
320                &AssetName::new(data_source_name),
321                &AssetLocation::null(),
322                &path_node_root_schema,
323            )
324            .unwrap();
325
326        //
327        // Create the data source and force full reload of it
328        //
329        let mut fs = FileSystemIdBasedDataSource::new(
330            file_system_root_path.clone(),
331            root_edit_context,
332            asset_source_id,
333        );
334        fs.load_from_storage(project_config, root_edit_context, import_job_to_queue);
335
336        self.data_sources.insert(asset_source_id, Box::new(fs));
337
338        asset_source_id
339    }
340
341    pub fn add_file_system_path_based_data_source<RootPathT: Into<PathBuf>>(
342        &mut self,
343        project_config: &HydrateProjectConfiguration,
344        data_source_name: &str,
345        file_system_root_path: RootPathT,
346        importer_registry: &ImporterRegistry,
347        import_jobs_to_queue: &mut ImportJobToQueue,
348    ) -> AssetSourceId {
349        let file_system_root_path = dunce::canonicalize(&file_system_root_path.into()).unwrap();
350        let path_node_root_schema = self.path_node_root_schema.as_record().unwrap().clone();
351        let root_edit_context = self.root_edit_context_mut();
352
353        // Commit any pending changes so we have a clean change tracking state
354        root_edit_context.commit_pending_undo_context();
355
356        //
357        // Create the PathNodeRoot asset that acts as the root location for all assets in this DS
358        //
359        let asset_source_id = AssetSourceId::new();
360        let root_asset_id = AssetId::from_uuid(*asset_source_id.uuid());
361        root_edit_context
362            .new_asset_with_id(
363                root_asset_id,
364                &AssetName::new(data_source_name),
365                &AssetLocation::null(),
366                &path_node_root_schema,
367            )
368            .unwrap();
369
370        //
371        // Create the data source and force full reload of it
372        //
373        let mut fs = FileSystemPathBasedDataSource::new(
374            file_system_root_path.clone(),
375            root_edit_context,
376            asset_source_id,
377            importer_registry,
378        );
379        fs.load_from_storage(project_config, root_edit_context, import_jobs_to_queue);
380
381        self.data_sources.insert(asset_source_id, Box::new(fs));
382
383        asset_source_id
384    }
385
386    pub fn save_root_edit_context(&mut self) {
387        //
388        // Ensure pending edits are flushed to the data set so that our modified assets list is fully up to date
389        //
390        let root_edit_context = self
391            .edit_contexts
392            .get_mut(self.root_edit_context_key)
393            .unwrap();
394        root_edit_context.commit_pending_undo_context();
395
396        for (_id, data_source) in &mut self.data_sources {
397            data_source.flush_to_storage(root_edit_context);
398        }
399    }
400
401    pub fn revert_root_edit_context(
402        &mut self,
403        project_config: &HydrateProjectConfiguration,
404        import_job_to_queue: &mut ImportJobToQueue,
405    ) {
406        //
407        // Ensure pending edits are cleared
408        //
409        let root_edit_context = self
410            .edit_contexts
411            .get_mut(self.root_edit_context_key)
412            .unwrap();
413        root_edit_context.cancel_pending_undo_context().unwrap();
414
415        //
416        // Take the contents of the modified asset list, leaving the edit context with a cleared list
417        //
418        for (_id, data_source) in &mut self.data_sources {
419            data_source.load_from_storage(project_config, root_edit_context, import_job_to_queue);
420        }
421
422        //
423        // Clear modified assets list since we reloaded everything from disk.
424        //
425        //root_edit_context.cancel_pending_undo_context();
426
427        //self.refresh_asset_path_lookups();
428        //self.refresh_location_tree();
429    }
430
431    pub fn close_file_system_source(
432        &mut self,
433        _asset_source_id: AssetSourceId,
434    ) {
435        unimplemented!();
436        // kill edit contexts or fail
437
438        // clear root_edit_context of data from this source
439
440        // drop the source
441        //let old = self.data_sources.remove(&asset_source_id);
442        //assert!(old.is_some());
443    }
444
445    // Spawns a separate edit context with copies of the given assets. The undo stack will be shared
446    // globally, but changes will not be visible on the root context. The edit context will be flushed
447    // to the root context in a single operation. Generally, we don't expect assets opened in a
448    // separate edit context to change in the root context, but there is nothing that prevents it.
449    pub fn open_edit_context(
450        &mut self,
451        assets: &[AssetId],
452    ) -> DataSetResult<EditContextKey> {
453        let new_edit_context_key = self.edit_contexts.insert_with_key(|key| {
454            EditContext::new_with_data(
455                &self.project_config,
456                key,
457                self.schema_set.clone(),
458                &self.undo_stack,
459            )
460        });
461
462        let [root_edit_context, new_edit_context] = self
463            .edit_contexts
464            .get_disjoint_mut([self.root_edit_context_key, new_edit_context_key])
465            .unwrap();
466
467        for asset in assets {
468            if !root_edit_context.assets().contains_key(asset) {
469                return Err(DataSetError::AssetNotFound)?;
470            }
471        }
472
473        for &asset_id in assets {
474            new_edit_context
475                .data_set
476                .copy_from(root_edit_context.data_set(), asset_id)
477                .expect("Could not copy asset to newly created edit context");
478        }
479
480        Ok(new_edit_context_key)
481    }
482
483    pub fn flush_edit_context_to_root(
484        &mut self,
485        edit_context: EditContextKey,
486    ) -> DataSetResult<()> {
487        assert_ne!(edit_context, self.root_edit_context_key);
488        let [root_context, context_to_flush] = self
489            .edit_contexts
490            .get_disjoint_mut([self.root_edit_context_key, edit_context])
491            .unwrap();
492
493        // In the case of failure we want to flush as much as we can, so keep the error around and
494        // return it after trying to flush all the assetsa
495        let mut first_error = None;
496        for &asset_id in context_to_flush.assets().keys() {
497            if let Err(e) = root_context
498                .data_set
499                .copy_from(&context_to_flush.data_set, asset_id)
500            {
501                if first_error.is_none() {
502                    first_error = Some(Err(e));
503                }
504            }
505        }
506
507        first_error.unwrap_or(Ok(()))
508    }
509
510    pub fn close_edit_context(
511        &mut self,
512        edit_context: EditContextKey,
513    ) {
514        assert_ne!(edit_context, self.root_edit_context_key);
515        self.edit_contexts.remove(edit_context);
516    }
517
518    pub fn undo(&mut self) -> DataSetResult<()> {
519        self.undo_stack.undo(&mut self.edit_contexts)
520    }
521
522    pub fn redo(&mut self) -> DataSetResult<()> {
523        self.undo_stack.redo(&mut self.edit_contexts)
524    }
525}