// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using FlaxEditor.Content; using FlaxEditor.CustomEditors; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; namespace FlaxEditor.Windows.Assets { /// /// Editor window to view/modify asset. /// /// /// public sealed class GameplayGlobalsWindow : AssetEditorWindowBase { private class AddRemoveParamAction : IUndoAction { public PropertiesProxy Proxy; public bool IsAdd; public string Name; public object DefaultValue; /// public string ActionString => IsAdd ? "Add parameter" : "Remove parameter"; /// public void Do() { if (IsAdd) Add(); else Remove(); } /// public void Undo() { if (IsAdd) Remove(); else Add(); } private void Add() { Proxy.DefaultValues[Name] = DefaultValue; Proxy.Window._propertiesEditor.BuildLayoutOnUpdate(); } private void Remove() { DefaultValue = Proxy.DefaultValues[Name]; Proxy.DefaultValues.Remove(Name); Proxy.Window._propertiesEditor.BuildLayoutOnUpdate(); } /// public void Dispose() { DefaultValue = null; Proxy = null; } } private class RenameParamAction : IUndoAction { public PropertiesProxy Proxy; public string Before; public string After; /// public string ActionString => "Rename parameter"; /// public void Do() { Rename(Before, After); } /// public void Undo() { Rename(After, Before); } private void Rename(string from, string to) { var defaultValue = Proxy.DefaultValues[from]; Proxy.DefaultValues.Remove(from); Proxy.DefaultValues[to] = defaultValue; Proxy.Window._propertiesEditor.BuildLayoutOnUpdate(); } /// public void Dispose() { Before = null; After = null; } } [CustomEditor(typeof(PropertiesProxyEditor))] private sealed class PropertiesProxy { [NoSerialize] public GameplayGlobalsWindow Window; [NoSerialize] public GameplayGlobals Asset; public Dictionary DefaultValues; public void Init(GameplayGlobalsWindow window) { Window = window; Asset = window.Asset; DefaultValues = Asset.DefaultValues; } } private sealed class VariableValueContainer : ValueContainer { private readonly PropertiesProxy _proxy; private readonly string _name; private readonly bool _isDefault; public VariableValueContainer(PropertiesProxy proxy, string name, object value, bool isDefault) : base(ScriptMemberInfo.Null, new ScriptType(value.GetType())) { _proxy = proxy; _name = name; _isDefault = isDefault; Add(value); } private object Getter(object instance, int index) { if (_isDefault) return _proxy.DefaultValues[_name]; return _proxy.Asset.GetValue(_name); } private void Setter(object instance, int index, object value) { if (_isDefault) _proxy.DefaultValues[_name] = value; else _proxy.Asset.SetValue(_name, value); } /// public override void Refresh(ValueContainer instanceValues) { if (instanceValues == null || instanceValues.Count != Count) throw new ArgumentException(); for (int i = 0; i < Count; i++) { var v = instanceValues[i]; this[i] = Getter(v, i); } } /// public override void Set(ValueContainer instanceValues, object value) { if (instanceValues == null || instanceValues.Count != Count) throw new ArgumentException(); for (int i = 0; i < Count; i++) { var v = instanceValues[i]; Setter(v, i, value); this[i] = value; } } /// public override void Set(ValueContainer instanceValues, ValueContainer values) { /*if (instanceValues == null || instanceValues.Count != Count) throw new ArgumentException(); if (values == null || values.Count != Count) throw new ArgumentException(); for (int i = 0; i < Count; i++) { var v = instanceValues[i]; var value = ((CustomValueContainer)values)[i]; Setter(v, i, value); this[i] = value; }*/ } /// public override void Set(ValueContainer instanceValues) { if (instanceValues == null || instanceValues.Count != Count) throw new ArgumentException(); for (int i = 0; i < Count; i++) { var v = instanceValues[i]; Setter(v, i, Getter(v, i)); } } /// public override void RefreshReferenceValue(object instanceValue) { // Not supported } } private sealed class PropertiesProxyEditor : CustomEditor { private PropertiesProxy _proxy; private ComboBox _addParamType; private static readonly Type[] AllowedTypes = { typeof(float), typeof(bool), typeof(int), typeof(Float2), typeof(Float3), typeof(Float4), typeof(Double2), typeof(Double3), typeof(Double4), typeof(Color), typeof(Quaternion), typeof(Transform), typeof(BoundingBox), typeof(BoundingSphere), typeof(Rectangle), typeof(Matrix), typeof(string), }; public override void Initialize(LayoutElementsContainer layout) { _proxy = (PropertiesProxy)Values[0]; if (_proxy?.DefaultValues == null) { layout.Label("Loading...", TextAlignment.Center); return; } var isPlayModeActive = _proxy.Window.Editor.StateMachine.IsPlayMode; if (isPlayModeActive) { layout.Label("Play mode is active. Editing runtime values.", TextAlignment.Center); layout.Space(10); foreach (var e in _proxy.DefaultValues) { var name = e.Key; var value = _proxy.Asset.GetValue(name); var valueContainer = new VariableValueContainer(_proxy, name, value, false); var propertyLabel = new PropertyNameLabel(name) { Tag = name, }; string tooltip = null; if (_proxy.DefaultValues.TryGetValue(name, out var defaultValue)) tooltip = "Default value: " + defaultValue; layout.Object(propertyLabel, valueContainer, null, tooltip); } } else { foreach (var e in _proxy.DefaultValues) { var name = e.Key; var value = e.Value; var valueContainer = new VariableValueContainer(_proxy, name, value, true); var propertyLabel = new ClickablePropertyNameLabel(name) { Tag = name, }; propertyLabel.MouseLeftDoubleClick += (label, location) => StartParameterRenaming(name, label); propertyLabel.SetupContextMenu += OnPropertyLabelSetupContextMenu; layout.Object(propertyLabel, valueContainer, null, "Type: " + CustomEditorsUtil.GetTypeNameUI(value.GetType())); } // TODO: improve the UI layout.Space(40); var addParamType = layout.ComboBox().ComboBox; object lastValue = null; foreach (var e in _proxy.DefaultValues) lastValue = e.Value; var allowedTypes = AllowedTypes.Select(CustomEditorsUtil.GetTypeNameUI).ToList(); int index = 0; if (lastValue != null) index = allowedTypes.FindIndex(x => x.Equals(CustomEditorsUtil.GetTypeNameUI(lastValue.GetType()), StringComparison.Ordinal)); addParamType.Items = allowedTypes; addParamType.SelectedIndex = index; _addParamType = addParamType; var addParamButton = layout.Button("Add").Button; addParamButton.Clicked += OnAddParamButtonClicked; } } private void OnAddParamButtonClicked() { AddParameter(AllowedTypes[_addParamType.SelectedIndex]); } private void OnPropertyLabelSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor) { var name = (string)label.Tag; menu.AddSeparator(); menu.AddButton("Rename", () => StartParameterRenaming(name, label)); menu.AddButton("Delete", () => DeleteParameter(name)); } private void AddParameter(Type type) { var asset = _proxy?.Asset; if (asset == null || asset.WaitForLoaded()) return; var action = new AddRemoveParamAction { Proxy = _proxy, IsAdd = true, Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)), DefaultValue = TypeUtils.GetDefaultValue(new ScriptType(type)), }; _proxy.Window.Undo.AddAction(action); action.Do(); } private void StartParameterRenaming(string name, Control label) { var dialog = RenamePopup.Show(label, new Rectangle(0, 0, label.Width - 2, label.Height), name, false); dialog.Tag = name; dialog.Validate += OnParameterRenameValidate; dialog.Renamed += OnParameterRenamed; } private bool OnParameterRenameValidate(RenamePopup popup, string value) { return !string.IsNullOrWhiteSpace(value) && !_proxy.DefaultValues.ContainsKey(value); } private void OnParameterRenamed(RenamePopup renamePopup) { var name = (string)renamePopup.Tag; var action = new RenameParamAction { Proxy = _proxy, Before = name, After = renamePopup.Text, }; _proxy.Window.Undo.AddAction(action); action.Do(); } private void DeleteParameter(string name) { var action = new AddRemoveParamAction { Proxy = _proxy, IsAdd = false, Name = name, }; _proxy.Window.Undo.AddAction(action); action.Do(); } } private CustomEditorPresenter _propertiesEditor; private PropertiesProxy _proxy; private ToolStripButton _saveButton; private ToolStripButton _undoButton; private ToolStripButton _redoButton; private ToolStripButton _resetButton; private Undo _undo; /// /// Gets the undo for asset editing actions. /// public Undo Undo => _undo; /// public GameplayGlobalsWindow(Editor editor, AssetItem item) : base(editor, item) { var inputOptions = Editor.Options.Options.Input; _undo = new Undo(); _undo.ActionDone += OnUndo; _undo.UndoDone += OnUndo; _undo.RedoDone += OnUndo; var panel = new Panel(ScrollBars.Vertical) { AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, _toolstrip.Bottom, 0), Parent = this, }; _propertiesEditor = new CustomEditorPresenter(_undo); _propertiesEditor.Panel.Parent = panel; _propertiesEditor.Modified += OnPropertiesEditorModified; _proxy = new PropertiesProxy(); _propertiesEditor.Select(_proxy); _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip($"Save asset ({inputOptions.Save})"); _toolstrip.AddSeparator(); _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _resetButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Rotate32, Reset).LinkTooltip("Resets the variables values to the default values"); InputActions.Add(options => options.Save, Save); InputActions.Add(options => options.Undo, _undo.PerformUndo); InputActions.Add(options => options.Redo, _undo.PerformRedo); } private void OnPropertiesEditorModified() { if (_proxy.Window.Editor.StateMachine.IsPlayMode) return; MarkAsEdited(); } private void OnUndo(IUndoAction action) { if (_proxy.Window.Editor.StateMachine.IsPlayMode) return; UpdateToolstrip(); MarkAsEdited(); } private void Reset() { _asset.ResetValues(); } /// protected override void OnAssetLoaded() { _undo.Clear(); _proxy.Init(this); _propertiesEditor.BuildLayoutOnUpdate(); UpdateToolstrip(); base.OnAssetLoaded(); } /// protected override void UnlinkItem() { _undo.Dispose(); base.UnlinkItem(); } /// protected override void UpdateToolstrip() { _saveButton.Enabled = IsEdited; _undoButton.Enabled = _undo.CanUndo; _redoButton.Enabled = _undo.CanRedo; _resetButton.Enabled = _asset != null; base.UpdateToolstrip(); } /// public override void OnPlayBegin() { base.OnPlayBegin(); if (IsEdited) { if (MessageBox.Show("Gameplay Globals asset has been modified. Save it before entering the play mode?", "Save gameplay globals?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { Save(); } } ClearEditedFlag(); _undo.Enabled = false; _undo.Clear(); _propertiesEditor.BuildLayoutOnUpdate(); UpdateToolstrip(); } /// public override void OnPlayEnd() { base.OnPlayEnd(); _undo.Enabled = true; _undo.Clear(); _propertiesEditor.BuildLayoutOnUpdate(); UpdateToolstrip(); } /// public override void Save() { if (!IsEdited) return; Asset.DefaultValues = _proxy.DefaultValues; if (Asset.Save()) { Editor.LogError("Cannot save asset."); return; } ClearEditedFlag(); } /// public override void OnDestroy() { base.OnDestroy(); _undo = null; _propertiesEditor = null; _proxy = null; _saveButton = null; _undoButton = null; _redoButton = null; _resetButton = null; } } }