// 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;
}
}
}