// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Xml; using FlaxEditor.Content; using FlaxEditor.CustomEditors; using FlaxEditor.Gizmo; using FlaxEditor.GUI; using FlaxEditor.GUI.Input; using FlaxEditor.SceneGraph; using FlaxEditor.Viewport; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Windows.Assets { /// /// Prefab window allows to view and edit asset. /// /// /// public sealed partial class PrefabWindow : AssetEditorWindowBase, IPresenterOwner { private readonly SplitPanel _split1; private readonly SplitPanel _split2; private readonly TextBox _searchBox; private readonly Panel _treePanel; private readonly PrefabTree _tree; private readonly PrefabWindowViewport _viewport; private readonly CustomEditorPresenter _propertiesEditor; private readonly ToolStripButton _saveButton; private readonly ToolStripButton _toolStripUndo; private readonly ToolStripButton _toolStripRedo; private readonly ToolStripButton _toolStripTranslate; private readonly ToolStripButton _toolStripRotate; private readonly ToolStripButton _toolStripScale; private readonly ToolStripButton _toolStripLiveReload; private Undo _undo; private bool _focusCamera; private bool _liveReload = false; private bool _isUpdatingSelection, _isScriptsReloading; private DateTime _modifiedTime = DateTime.MinValue; /// /// Gets the prefab hierarchy tree control. /// public PrefabTree Tree => _tree; /// /// Gets the viewport. /// public PrefabWindowViewport Viewport => _viewport; /// /// Gets the prefab objects properties editor. /// public CustomEditorPresenter Presenter => _propertiesEditor; /// /// Gets the undo system used by this window for changes tracking. /// public Undo Undo => _undo; /// /// The local scene nodes graph used by the prefab editor. /// public readonly LocalSceneGraph Graph; /// /// Gets or sets a value indicating whether use live reloading for the prefab changes (applies prefab changes on modification by auto). /// public bool LiveReload { get => _liveReload; set { if (_liveReload != value) { _liveReload = value; UpdateToolstrip(); } } } /// /// Gets or sets the live reload timeout. It defines the time to apply prefab changes after modification. /// public TimeSpan LiveReloadTimeout { get; set; } = TimeSpan.FromMilliseconds(100); /// public PrefabWindow(Editor editor, AssetItem item) : base(editor, item) { // Undo _undo = new Undo(); _undo.UndoDone += OnUndoEvent; _undo.RedoDone += OnUndoEvent; _undo.ActionDone += OnUndoEvent; // Split Panel 1 _split1 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.None) { AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, _toolstrip.Bottom, 0), SplitterValue = 0.2f, Parent = this }; var sceneTreePanel = new SceneTreePanel(this) { Parent = _split1.Panel1, }; // Split Panel 2 _split2 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical) { AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, SplitterValue = 0.6f, Parent = _split1.Panel2 }; // Prefab structure tree searching query input box var headerPanel = new ContainerControl { AnchorPreset = AnchorPresets.HorizontalStretchTop, BackgroundColor = Style.Current.Background, IsScrollable = false, Offsets = new Margin(0, 0, 0, 18 + 6), }; _searchBox = new SearchBox() { AnchorPreset = AnchorPresets.HorizontalStretchMiddle, Parent = headerPanel, Bounds = new Rectangle(4, 4, headerPanel.Width - 8, 18), }; _searchBox.TextChanged += OnSearchBoxTextChanged; _treePanel = new Panel() { AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0.0f, 0.0f, headerPanel.Bottom, 0.0f), ScrollBars = ScrollBars.Both, IsScrollable = true, Parent = sceneTreePanel, }; // Prefab structure tree Graph = new LocalSceneGraph(new CustomRootNode(this)); _tree = new PrefabTree { Margin = new Margin(0.0f, 0.0f, -16.0f, _treePanel.ScrollBarsSize), // Hide root node IsScrollable = true, }; _tree.AddChild(Graph.Root.TreeNode); _tree.SelectedChanged += OnTreeSelectedChanged; _tree.RightClick += OnTreeRightClick; _tree.Parent = _treePanel; headerPanel.Parent = sceneTreePanel; // Prefab viewport _viewport = new PrefabWindowViewport(this) { Parent = _split2.Panel1 }; _viewport.TransformGizmo.ModeChanged += UpdateToolstrip; // Prefab properties editor _propertiesEditor = new CustomEditorPresenter(_undo, null, this); _propertiesEditor.Panel.Parent = _split2.Panel2; _propertiesEditor.Modified += MarkAsEdited; // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); _toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); _toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); _toolstrip.AddSeparator(); _toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip("Change Gizmo tool mode to Translate (1)"); _toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip("Change Gizmo tool mode to Rotate (2)"); _toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip("Change Gizmo tool mode to Scale (3)"); _toolstrip.AddSeparator(); _toolStripLiveReload = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, () => LiveReload = !LiveReload).SetChecked(true).SetAutoCheck(true).LinkTooltip("Live changes preview (applies prefab changes on modification by auto)"); Editor.Prefabs.PrefabApplied += OnPrefabApplied; ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd; // Setup input actions InputActions.Add(options => options.Undo, () => { _undo.PerformUndo(); Focus(); }); InputActions.Add(options => options.Redo, () => { _undo.PerformRedo(); Focus(); }); InputActions.Add(options => options.Cut, Cut); InputActions.Add(options => options.Copy, Copy); InputActions.Add(options => options.Paste, Paste); InputActions.Add(options => options.Duplicate, Duplicate); InputActions.Add(options => options.Delete, Delete); InputActions.Add(options => options.Rename, Rename); InputActions.Add(options => options.FocusSelection, _viewport.FocusSelection); } /// /// Enables or disables vertical and horizontal scrolling on the tree panel. /// /// The state to set scrolling to public void ScrollingOnTreeView(bool enabled) { if (_treePanel.VScrollBar != null) _treePanel.VScrollBar.ThumbEnabled = enabled; if (_treePanel.HScrollBar != null) _treePanel.HScrollBar.ThumbEnabled = enabled; } /// /// Scrolls to the selected node in the tree. /// public void ScrollToSelectedNode() { // Scroll to node var nodeSelection = _tree.Selection; if (nodeSelection.Count != 0) { var scrollControl = nodeSelection[nodeSelection.Count - 1]; _treePanel.ScrollViewTo(scrollControl); } } private void OnSearchBoxTextChanged() { // Skip events during setup or init stuff if (IsLayoutLocked) return; _tree.LockChildrenRecursive(); // Update tree var query = _searchBox.Text; var root = Graph.Root; root.TreeNode.UpdateFilter(query); _tree.UnlockChildrenRecursive(); PerformLayout(); PerformLayout(); } /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (base.OnMouseUp(location, button)) return true; if (button == MouseButton.Right && _treePanel.ContainsPoint(ref location)) { _tree.Deselect(); var locationCM = location + _searchBox.BottomLeft; ShowContextMenu(Parent, ref locationCM); return true; } if (button == MouseButton.Left && _treePanel.ContainsPoint(ref location)) { _tree.Deselect(); return true; } return false; } private void OnScriptsReloadBegin() { _isScriptsReloading = true; if (_asset == null || !_asset.IsLoaded) return; Editor.Log("Reloading prefab editor data on scripts reload. Prefab: " + _asset.Path); // Check if asset has been edited and not saved (and still has linked item) if (IsEdited) { // Ask user for further action var result = MessageBox.Show( string.Format("Asset \'{0}\' has been edited. Save before scripts reload?", _item.Path), "Save before reloading?", MessageBoxButtons.YesNo ); if (result == DialogResult.OK || result == DialogResult.Yes) { Save(); } } // Cleanup Deselect(); Graph.MainActor = null; _viewport.Prefab = null; _undo?.Clear(); // TODO: maybe don't clear undo? } private void OnScriptsReloadEnd() { _isScriptsReloading = false; if (_asset == null || !_asset.IsLoaded) return; // Restore _viewport.Prefab = _asset; Graph.MainActor = _viewport.Instance; Selection.Clear(); Select(Graph.Main); Graph.Root.TreeNode.ExpandAll(true); _undo.Clear(); ClearEditedFlag(); } private void OnUndoEvent(IUndoAction action) { // All undo actions modify the asset except selection change if (!(action is SelectionChangeAction)) { MarkAsEdited(); } UpdateToolstrip(); } private void OnPrefabApplied(Prefab prefab, Actor instance) { if (prefab == Asset) { ClearEditedFlag(); _item.RefreshThumbnail(); } } /// public override void Save() { // Check if don't need to push any new changes to the original asset if (!IsEdited) return; try { // Simply update changes Editor.Prefabs.ApplyAll(_viewport.Instance); // Refresh properties panel to sync new prefab default values _propertiesEditor.BuildLayout(); } catch (Exception) { // Disable live reload on error if (LiveReload) { LiveReload = false; } throw; } } /// protected override void UpdateToolstrip() { var undoRedo = _undo; var gizmo = _viewport.TransformGizmo; _saveButton.Enabled = IsEdited; _toolStripUndo.Enabled = undoRedo.CanUndo; _toolStripRedo.Enabled = undoRedo.CanRedo; // var gizmoMode = gizmo.ActiveMode; _toolStripTranslate.Checked = gizmoMode == TransformGizmoBase.Mode.Translate; _toolStripRotate.Checked = gizmoMode == TransformGizmoBase.Mode.Rotate; _toolStripScale.Checked = gizmoMode == TransformGizmoBase.Mode.Scale; // _toolStripLiveReload.Checked = _liveReload; base.UpdateToolstrip(); } /// protected override void OnEditedState() { base.OnEditedState(); _modifiedTime = DateTime.Now; } /// protected override void OnAssetLoaded() { // Skip during scripts reload to prevent issues if (_isScriptsReloading) { base.OnAssetLoaded(); return; } _viewport.Prefab = _asset; Graph.MainActor = _viewport.Instance; _focusCamera = true; Selection.Clear(); Select(Graph.Main); Graph.Root.TreeNode.ExpandAll(true); _undo.Clear(); ClearEditedFlag(); base.OnAssetLoaded(); } /// protected override void UnlinkItem() { Deselect(); Graph.MainActor = null; _viewport.Prefab = null; _undo?.Clear(); base.UnlinkItem(); } /// public override void Update(float deltaTime) { try { if (Graph.Main != null) { // Due to fact that actors in prefab editor are only created but not added to gameplay // we have to manually update some data (Level events work only for actors in a gameplay) Update(Graph.Main); } base.Update(deltaTime); } catch (Exception ex) { // Info Editor.LogWarning("Error when updating prefab window for " + _asset); Editor.LogWarning(ex); // Refresh Deselect(); Graph.MainActor = null; _viewport.Prefab = null; if (_asset.IsLoaded) { _viewport.Prefab = _asset; Graph.MainActor = _viewport.Instance; Selection.Clear(); Select(Graph.Main); Graph.Root.TreeNode.ExpandAll(true); } } // Auto fit if (_focusCamera && _viewport.Task.FrameCount > 1) { _focusCamera = false; Editor.GetActorEditorSphere(_viewport.Instance, out BoundingSphere bounds); _viewport.ViewPosition = bounds.Center - _viewport.ViewDirection * (bounds.Radius * 1.2f); } // Auto save if (IsEdited && LiveReload && DateTime.Now - _modifiedTime > LiveReloadTimeout) { Save(); } } /// public override bool UseLayoutData => true; /// public override void OnLayoutSerialize(XmlWriter writer) { LayoutSerializeSplitter(writer, "Split1", _split1); LayoutSerializeSplitter(writer, "Split2", _split2); writer.WriteAttributeString("LiveReload", LiveReload.ToString()); writer.WriteAttributeString("GizmoMode", Viewport.TransformGizmo.ActiveMode.ToString()); } /// public override void OnLayoutDeserialize(XmlElement node) { LayoutDeserializeSplitter(node, "Split1", _split1); LayoutDeserializeSplitter(node, "Split2", _split2); if (bool.TryParse(node.GetAttribute("LiveReload"), out bool value2)) LiveReload = value2; if (Enum.TryParse(node.GetAttribute("GizmoMode"), out TransformGizmoBase.Mode value3)) Viewport.TransformGizmo.ActiveMode = value3; } /// public override void OnLayoutDeserialize() { _split1.SplitterValue = 0.2f; _split2.SplitterValue = 0.6f; LiveReload = false; } /// public override void OnDestroy() { Editor.Prefabs.PrefabApplied -= OnPrefabApplied; ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd; _undo.Dispose(); Graph.Dispose(); base.OnDestroy(); } /// public EditorViewport PresenterViewport => _viewport; } }