// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using FlaxEditor.Content; using FlaxEditor.GUI; using FlaxEditor.GUI.ContextMenu; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Windows.Assets { /// /// Base class for assets editing/viewing windows. /// /// public abstract class AssetEditorWindow : EditorWindow, IEditable, IContentItemOwner { /// /// The item. /// protected AssetItem _item; /// /// The toolstrip. /// protected readonly ToolStrip _toolstrip; /// /// Gets the item. /// public AssetItem Item => _item; /// /// Gets the toolstrip UI. /// public ToolStrip ToolStrip => _toolstrip; /// public override string SerializationTypename => _item.ID.ToString(); /// /// Initializes a new instance of the class. /// /// The editor. /// The item. protected AssetEditorWindow(Editor editor, AssetItem item) : base(editor, false, ScrollBars.None) { _item = item ?? throw new ArgumentNullException(nameof(item)); _item.AddReference(this); _toolstrip = new ToolStrip { Parent = this }; _toolstrip.AddButton(editor.Icons.Search64, () => Editor.Windows.ContentWin.Select(_item)).LinkTooltip("Show and select in Content Window"); InputActions.Add(options => options.Save, Save); UpdateTitle(); } /// /// Unlinks the item. Removes reference to it and unbinds all events. /// protected virtual void UnlinkItem() { _item.RemoveReference(this); _item = null; } /// /// Updates the toolstrip buttons and other controls. Called after some window events. /// protected virtual void UpdateToolstrip() { } /// /// Gets the name of the window title format text ({0} to insert asset short name). /// protected virtual string WindowTitleName => "{0}"; /// /// Updates the window title text. /// protected void UpdateTitle() { string title = string.Format(WindowTitleName, _item?.ShortName ?? string.Empty); if (IsEdited) title += '*'; Title = title; } /// /// Tries to save asset changes if it has been edited. /// public virtual void Save() { } /// public override bool IsEditingItem(ContentItem item) { return item == _item; } /// protected override bool OnClosing(ClosingReason reason) { // Block closing only on user events if (reason == ClosingReason.User) { // Check if asset has been edited and not saved (and still has linked item) if (IsEdited && _item != null) { // Ask user for further action var result = MessageBox.Show( string.Format("Asset \'{0}\' has been edited. Save before closing?", _item.Path), "Save before closing?", MessageBoxButtons.YesNoCancel ); if (result == DialogResult.OK || result == DialogResult.Yes) { // Save and close Save(); } else if (result == DialogResult.Cancel || result == DialogResult.Abort) { // Cancel closing return true; } } } return base.OnClosing(reason); } /// protected override void OnClose() { if (_item != null) { // Ensure to remove linkage to the item UnlinkItem(); } base.OnClose(); } /// public override void OnDestroy() { if (_item != null) { // Ensure to remove linkage to the item UnlinkItem(); } base.OnDestroy(); } #region IEditable Implementation private bool _isEdited; /// /// Occurs when object gets edited. /// public event Action OnEdited; /// public bool IsEdited { get => _isEdited; protected set { if (value) MarkAsEdited(); else ClearEditedFlag(); } } /// public void MarkAsEdited() { // Check if state will change if (_isEdited == false) { // Set flag _isEdited = true; // Call events OnEditedState(); OnEdited?.Invoke(); OnEditedStateChanged(); } } /// /// Clears the edited flag. /// protected void ClearEditedFlag() { // Check if state will change if (_isEdited) { // Clear flag _isEdited = false; // Call event OnEditedStateChanged(); } } /// /// Action fired when object gets edited. /// protected virtual void OnEditedState() { } /// /// Action fired when object edited state gets changed. /// protected virtual void OnEditedStateChanged() { UpdateTitle(); UpdateToolstrip(); } /// public override void OnShowContextMenu(ContextMenu menu) { base.OnShowContextMenu(menu); menu.AddButton("Save", Save).Enabled = IsEdited; menu.AddButton("Copy name", () => Clipboard.Text = Item.NamePath); menu.AddSeparator(); } #endregion #region IContentItemOwner Implementation /// public void OnItemDeleted(ContentItem item) { if (item == _item) { Close(); } } /// public void OnItemRenamed(ContentItem item) { if (item == _item) { UpdateTitle(); } } /// public virtual void OnItemReimported(ContentItem item) { } /// public void OnItemDispose(ContentItem item) { if (item == _item) { Close(); } } #endregion } /// /// Generic base class for asset editors. /// /// Asset type. /// public abstract class AssetEditorWindowBase : AssetEditorWindow where T : Asset { /// /// Flag set to true if window is waiting for asset to be loaded (to send or events). /// protected bool _isWaitingForLoaded; /// /// The asset reference. /// protected T _asset; /// /// Gets the asset. /// public T Asset => _asset; /// protected AssetEditorWindowBase(Editor editor, AssetItem item) : base(editor, item) { } /// /// Drops any loaded asset data and refreshes the UI state. /// public void RefreshAsset() { if (_asset == null || _asset.WaitForLoaded()) return; OnAssetLoaded(); MarkAsEdited(); Save(); } /// /// Reloads the asset (window will receive or events). /// public void ReloadAsset() { _asset.Reload(); _isWaitingForLoaded = true; } /// /// Loads the asset. /// /// Loaded asset or null if cannot do it. protected virtual T LoadAsset() { return FlaxEngine.Content.LoadAsync(_item.Path); } /// /// Called when asset gets linked and window can setup UI for it. /// protected virtual void OnAssetLinked() { } /// /// Called when asset gets loaded and window can setup UI for it. /// protected virtual void OnAssetLoaded() { } /// /// Called when asset fails to load and window can setup UI for it. /// protected virtual void OnAssetLoadFailed() { } /// public override void Update(float deltaTime) { if (_isWaitingForLoaded) { if (_asset == null) { _isWaitingForLoaded = false; } else if (_asset.IsLoaded) { _isWaitingForLoaded = false; OnAssetLoaded(); } else if (_asset.LastLoadFailed) { _isWaitingForLoaded = false; OnAssetLoadFailed(); } } base.Update(deltaTime); } /// protected override void OnShow() { // Check if has no asset (but has item linked) if (_asset == null && _item != null) { // Load asset _asset = LoadAsset(); if (_asset == null) { Editor.LogError(string.Format("Cannot load asset \'{0}\' ({1})", _item.Path, typeof(T))); Close(); return; } // Fire event OnAssetLinked(); _isWaitingForLoaded = true; } // Base base.OnShow(); // Update UpdateTitle(); UpdateToolstrip(); PerformLayout(); } /// protected override void UnlinkItem() { _asset = null; base.UnlinkItem(); } /// public override void OnItemReimported(ContentItem item) { // Wait for loaded after reimport _isWaitingForLoaded = true; base.OnItemReimported(item); } } /// /// Generic base class for asset editors that modify cloned asset and update original asset on save. /// /// Asset type. /// public abstract class ClonedAssetEditorWindowBase : AssetEditorWindowBase where T : Asset { // TODO: delete cloned asset on usage end? /// /// Gets the original asset. Note: is the cloned asset for local editing. Use to apply changes to the original asset. /// public T OriginalAsset => (T)FlaxEngine.Content.Load(_item.ID); /// protected ClonedAssetEditorWindowBase(Editor editor, AssetItem item) : base(editor, item) { } /// /// Saves the copy of the asset to the original location. This action cannot be undone! /// /// True if failed, otherwise false. protected virtual bool SaveToOriginal() { // Wait until temporary asset file be fully loaded if (_asset.WaitForLoaded()) { Editor.LogError(string.Format("Cannot save asset {0}. Wait for temporary asset loaded failed.", _item.Path)); return true; } // Cache data var id = _item.ID; var sourcePath = _asset.Path; var destinationPath = _item.Path; // Check if original asset is loaded var originalAsset = (T)FlaxEngine.Content.GetAsset(id); if (originalAsset) { // Wait for loaded to prevent any issues if (!originalAsset.IsLoaded && originalAsset.LastLoadFailed) { Editor.LogWarning(string.Format("Copying asset \'{0}\' to \'{1}\' (last load failed)", sourcePath, destinationPath)); } else if (originalAsset.WaitForLoaded()) { Editor.LogError(string.Format("Cannot save asset {0}. Wait for original asset loaded failed.", _item.Path)); return true; } } // Copy temporary material to the final destination (and restore ID) if (Editor.ContentEditing.CloneAssetFile(sourcePath, destinationPath, id)) { Editor.LogError(string.Format("Cannot copy asset \'{0}\' to \'{1}\'", sourcePath, destinationPath)); return true; } // Reload original asset if (originalAsset) { originalAsset.Reload(); } // Refresh thumbnail _item.RefreshThumbnail(); return false; } /// protected override T LoadAsset() { // Clone asset if (Editor.ContentEditing.FastTempAssetClone(_item.Path, out var clonePath)) return null; // Load cloned asset var asset = FlaxEngine.Content.LoadAsync(clonePath); if (asset == null) return null; // Validate data if (asset.ID == _item.ID) throw new InvalidOperationException("Cloned asset has the same IDs."); return asset; } } }