#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using OpenRA.Graphics; using OpenRA.Mods.Common.Traits; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic { public class ActorEditLogic : ChromeLogic { [FluentReference] const string DuplicateActorId = "label-duplicate-actor-id"; [FluentReference] const string EnterActorId = "label-actor-id"; [FluentReference] const string Owner = "label-actor-owner"; // Error states define overlapping bits to simplify panel reflow logic [Flags] enum ActorIDStatus { Normal = 0, Duplicate = 1, Empty = 3 } readonly WorldRenderer worldRenderer; readonly EditorActorLayer editorActorLayer; readonly EditorActionManager editorActionManager; readonly EditorViewportControllerWidget editor; readonly Widget actorEditPanel; readonly LabelWidget typeLabel; readonly TextFieldWidget actorIDField; readonly HashSet typableFields = new(); readonly LabelWidget actorIDErrorLabel; readonly Widget initContainer; readonly Widget buttonContainer; readonly Widget checkboxOptionTemplate; readonly Widget sliderOptionTemplate; readonly Widget dropdownOptionTemplate; ActorIDStatus actorIDStatus = ActorIDStatus.Normal; ActorIDStatus nextActorIDStatus = ActorIDStatus.Normal; string initialActorID; EditActorPreview editActorPreview; EditorActorPreview SelectedActor => editor.DefaultBrush.Selection.Actor; internal bool IsChangingSelection { get; set; } [ObjectCreator.UseCtor] public ActorEditLogic(Widget widget, World world, WorldRenderer worldRenderer, Dictionary logicArgs) { this.worldRenderer = worldRenderer; editorActorLayer = world.WorldActor.Trait(); editorActionManager = world.WorldActor.Trait(); editor = widget.Parent.Parent.Get("MAP_EDITOR"); editor.DefaultBrush.SelectionChanged += HandleSelectionChanged; var selectTabContainer = widget.Parent.Parent.Get("SELECT_WIDGETS"); actorEditPanel = selectTabContainer.Get("ACTOR_EDIT_PANEL"); typeLabel = actorEditPanel.Get("ACTOR_TYPE_LABEL"); actorIDField = actorEditPanel.Get("ACTOR_ID"); initContainer = actorEditPanel.Get("ACTOR_INIT_CONTAINER"); buttonContainer = actorEditPanel.Get("BUTTON_CONTAINER"); checkboxOptionTemplate = initContainer.Get("CHECKBOX_OPTION_TEMPLATE"); sliderOptionTemplate = initContainer.Get("SLIDER_OPTION_TEMPLATE"); dropdownOptionTemplate = initContainer.Get("DROPDOWN_OPTION_TEMPLATE"); initContainer.RemoveChildren(); var deleteButton = actorEditPanel.Get("DELETE_BUTTON"); var cancelButton = actorEditPanel.Get("CANCEL_BUTTON"); var okButton = actorEditPanel.Get("OK_BUTTON"); actorIDErrorLabel = actorEditPanel.Get("ACTOR_ID_ERROR_LABEL"); actorIDErrorLabel.IsVisible = () => actorIDStatus != ActorIDStatus.Normal; actorIDErrorLabel.GetText = () => actorIDStatus == ActorIDStatus.Duplicate || nextActorIDStatus == ActorIDStatus.Duplicate ? FluentProvider.GetMessage(DuplicateActorId) : FluentProvider.GetMessage(EnterActorId); okButton.IsDisabled = () => !IsValid() || editActorPreview == null || !editActorPreview.IsDirty; okButton.OnClick = Save; cancelButton.OnClick = Cancel; deleteButton.OnClick = Delete; actorEditPanel.IsVisible = () => editor.CurrentBrush == editor.DefaultBrush && SelectedActor != null; actorIDField.OnEscKey = _ => actorIDField.YieldKeyboardFocus(); actorIDField.OnTextEdited = () => { var actorId = actorIDField.Text.Trim(); if (string.IsNullOrWhiteSpace(actorId)) { nextActorIDStatus = ActorIDStatus.Empty; return; } // Check for duplicate actor ID if (!SelectedActor.ID.Equals(actorId, StringComparison.OrdinalIgnoreCase) && editorActorLayer[actorId] != null) { nextActorIDStatus = ActorIDStatus.Duplicate; actorIDErrorLabel.Visible = true; return; } SetActorID(actorId); nextActorIDStatus = ActorIDStatus.Normal; }; actorIDField.OnLoseFocus = () => { // Reset invalid IDs back to their starting value if (actorIDStatus != ActorIDStatus.Normal) SetActorID(initialActorID); }; } void SetActorID(string actorId) { editActorPreview.SetActorID(actorId); nextActorIDStatus = ActorIDStatus.Normal; } bool IsValid() { return nextActorIDStatus == ActorIDStatus.Normal; } protected override void Dispose(bool disposing) { editor.DefaultBrush.SelectionChanged -= HandleSelectionChanged; base.Dispose(disposing); } void HandleSelectionChanged() { if (SelectedActor != null) { // Our edit control is updating the selection to account for an actor ID change. // Don't try and reset, we're the ones who instigated the selection change! if (!IsChangingSelection) Reset(); editActorPreview = new EditActorPreview(this, editor, editorActorLayer, SelectedActor); initialActorID = actorIDField.Text = SelectedActor.ID; var font = Game.Renderer.Fonts[typeLabel.Font]; var truncatedType = WidgetUtils.TruncateText(FluentProvider.GetMessage(SelectedActor.DescriptiveName), typeLabel.Bounds.Width, font); typeLabel.GetText = () => truncatedType; actorIDField.CursorPosition = SelectedActor.ID.Length; nextActorIDStatus = ActorIDStatus.Normal; // Remove old widgets var oldInitHeight = initContainer.Bounds.Height; initContainer.Bounds.Height = 0; initContainer.RemoveChildren(); // Add owner dropdown var ownerContainer = dropdownOptionTemplate.Clone(); var owner = FluentProvider.GetMessage(Owner); ownerContainer.Get("LABEL").GetText = () => owner; var ownerDropdown = ownerContainer.Get("OPTION"); var selectedOwner = SelectedActor.Owner; void UpdateOwner(EditorActorPreview preview, PlayerReference reference) { preview.Owner = reference; preview.ReplaceInit(new OwnerInit(reference.Name)); } var ownerHandler = new EditorActorOptionActionHandle(UpdateOwner, SelectedActor.Owner); editActorPreview.Add(ownerHandler); ScrollItemWidget SetupItem(PlayerReference option, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, () => selectedOwner == option, () => { selectedOwner = option; UpdateOwner(SelectedActor, selectedOwner); ownerHandler.OnChange(option); }); item.Get("LABEL").GetText = () => option.Name; item.GetColor = () => option.Color; return item; } ownerDropdown.GetText = () => selectedOwner.Name; ownerDropdown.GetColor = () => selectedOwner.Color; ownerDropdown.OnClick = () => { var owners = editorActorLayer.Players.Players.Values.OrderBy(p => p.Name); ownerDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 270, owners, SetupItem); }; initContainer.Bounds.Height += ownerContainer.Bounds.Height; initContainer.AddChild(ownerContainer); // Add new children for inits var options = SelectedActor.Info.TraitInfos() .SelectMany(t => t.ActorOptions(SelectedActor.Info, worldRenderer.World)) .OrderBy(o => o.DisplayOrder); foreach (var o in options) { if (o is EditorActorCheckbox co) { var checkboxContainer = checkboxOptionTemplate.Clone(); checkboxContainer.Bounds.Y = initContainer.Bounds.Height; initContainer.Bounds.Height += checkboxContainer.Bounds.Height; var checkbox = checkboxContainer.Get("OPTION"); checkbox.GetText = () => co.Name; var editorActionHandle = new EditorActorOptionActionHandle(co.OnChange, co.GetValue(SelectedActor)); editActorPreview.Add(editorActionHandle); checkbox.IsChecked = () => co.GetValue(SelectedActor); checkbox.OnClick = () => { var newValue = co.GetValue(SelectedActor) ^ true; co.OnChange(SelectedActor, newValue); editorActionHandle.OnChange(newValue); }; initContainer.AddChild(checkboxContainer); } else if (o is EditorActorSlider so) { var sliderContainer = sliderOptionTemplate.Clone(); sliderContainer.Bounds.Y = initContainer.Bounds.Height; initContainer.Bounds.Height += sliderContainer.Bounds.Height; sliderContainer.Get("LABEL").GetText = () => so.Name; var slider = sliderContainer.Get("OPTION"); slider.MinimumValue = so.MinValue; slider.MaximumValue = so.MaxValue; slider.Ticks = so.Ticks; var editorActionHandle = new EditorActorOptionActionHandle(so.OnChange, so.GetValue(SelectedActor)); editActorPreview.Add(editorActionHandle); slider.GetValue = () => so.GetValue(SelectedActor); slider.OnChange += value => so.OnChange(SelectedActor, value); slider.OnChange += value => editorActionHandle.OnChange(value); var valueField = sliderContainer.GetOrNull("VALUE"); if (valueField != null) { void UpdateValueField(float f) => valueField.Text = ((int)f).ToString(NumberFormatInfo.CurrentInfo); UpdateValueField(so.GetValue(SelectedActor)); slider.OnChange += UpdateValueField; valueField.OnTextEdited = () => { if (float.TryParse(valueField.Text, out var result)) slider.UpdateValue(result); }; valueField.OnEscKey = _ => { valueField.YieldKeyboardFocus(); return true; }; valueField.OnEnterKey = _ => { valueField.YieldKeyboardFocus(); return true; }; typableFields.Add(valueField); } initContainer.AddChild(sliderContainer); } else if (o is EditorActorDropdown ddo) { var dropdownContainer = dropdownOptionTemplate.Clone(); dropdownContainer.Bounds.Y = initContainer.Bounds.Height; initContainer.Bounds.Height += dropdownContainer.Bounds.Height; dropdownContainer.Get("LABEL").GetText = () => ddo.Name; var labels = ddo.GetLabels(SelectedActor); var editorActionHandle = new EditorActorOptionActionHandle(ddo.OnChange, ddo.GetValue(SelectedActor, labels)); editActorPreview.Add(editorActionHandle); var dropdown = dropdownContainer.Get("OPTION"); ScrollItemWidget DropdownSetup(KeyValuePair option, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, () => ddo.GetValue(SelectedActor, labels) == option.Key, () => { ddo.OnChange(SelectedActor, option.Key); editorActionHandle.OnChange(option.Key); }); item.Get("LABEL").GetText = () => option.Value; return item; } dropdown.GetText = () => labels[ddo.GetValue(SelectedActor, labels)]; dropdown.OnClick = () => dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 270, labels, DropdownSetup); initContainer.AddChild(dropdownContainer); } } buttonContainer.Bounds.Y += initContainer.Bounds.Height - oldInitHeight; } else { // Selected actor is null, hide the border and edit panel. Close(); } } public override void Tick() { if (actorIDStatus != nextActorIDStatus) { if ((actorIDStatus & nextActorIDStatus) == 0) { var offset = actorIDErrorLabel.Bounds.Height; if (nextActorIDStatus == ActorIDStatus.Normal) offset *= -1; initContainer.Bounds.Y += offset; buttonContainer.Bounds.Y += offset; } actorIDStatus = nextActorIDStatus; } } void Delete() { YieldFocus(); if (SelectedActor != null) editorActionManager.Add(new RemoveSelectedActorAction( editor.DefaultBrush, editorActorLayer, SelectedActor)); } void Cancel() { Reset(); Close(); } void Reset() { editActorPreview?.Reset(); } void YieldFocus() { actorIDField.YieldKeyboardFocus(); foreach (var f in typableFields) f.YieldKeyboardFocus(); } void Close() { YieldFocus(); if (SelectedActor != null) { editor.DefaultBrush.ClearSelection(updateSelectedTab: true); } } void Save() { editorActionManager.Add(new EditActorEditorAction(SelectedActor, editActorPreview.GetDirtyHandles())); editActorPreview = null; Close(); } } public class EditorActorOptionActionHandle : IEditActorHandle { readonly Action change; T value; readonly T initialValue; public EditorActorOptionActionHandle(Action change, T value) { this.change = change; this.value = value; initialValue = value; } public void OnChange(T value) { IsDirty = !EqualityComparer.Default.Equals(initialValue, value); this.value = value; } public void Do(ref EditorActorPreview actor) { change(actor, value); } public void Undo(ref EditorActorPreview actor) { change(actor, initialValue); } public bool IsDirty { get; private set; } public bool ShouldDoOnSave => false; } public interface IEditActorHandle { void Do(ref EditorActorPreview actor); void Undo(ref EditorActorPreview actor); bool IsDirty { get; } bool ShouldDoOnSave { get; } } sealed class EditActorEditorAction : IEditorAction { [FluentReference("name", "id")] const string EditedActor = "notification-edited-actor"; [FluentReference("name", "old-id", "new-id")] const string EditedActorId = "notification-edited-actor-id"; public string Text { get; private set; } public EditorActorPreview Actor; readonly IEnumerable handles; public EditActorEditorAction(EditorActorPreview actor, IEnumerable handles) { Actor = actor; this.handles = handles; Text = FluentProvider.GetMessage(EditedActor, "name", actor.Info.Name, "id", actor.ID); } public void Execute() { var before = Actor; foreach (var editorActionHandle in handles.Where(h => h.ShouldDoOnSave)) editorActionHandle.Do(ref Actor); var after = Actor; if (before != after) Text = FluentProvider.GetMessage(EditedActorId, "name", after.Info.Name, "old-id", before.ID, "new-id", after.ID); } public void Do() { foreach (var editorActionHandle in handles) editorActionHandle.Do(ref Actor); } public void Undo() { foreach (var editorActionHandle in handles) editorActionHandle.Undo(ref Actor); } } sealed class EditActorPreview { readonly SetActorIdAction setActorIdAction; readonly List handles = new(); EditorActorPreview actor; public EditActorPreview(ActorEditLogic logic, EditorViewportControllerWidget editor, EditorActorLayer editorActorLayer, EditorActorPreview actor) { this.actor = actor; setActorIdAction = new SetActorIdAction(logic, editor, editorActorLayer, actor.ID); handles.Add(setActorIdAction); } public bool IsDirty { get { return handles.Any(h => h.IsDirty); } } public void SetActorID(string actorID) { setActorIdAction.Set(actorID); } public void Add(IEditActorHandle editActor) { handles.Add(editActor); } public IEnumerable GetDirtyHandles() { return handles.Where(h => h.IsDirty); } public void Reset() { foreach (var handle in handles.Where(h => h.IsDirty)) handle.Undo(ref actor); } } public class SetActorIdAction : IEditActorHandle { readonly ActorEditLogic logic; readonly EditorViewportControllerWidget editor; readonly EditorActorLayer editorActorLayer; readonly string initial; string newID; public void Set(string actorId) { IsDirty = initial != actorId; newID = actorId; } public SetActorIdAction(ActorEditLogic logic, EditorViewportControllerWidget editor, EditorActorLayer editorActorLayer, string initial) { this.logic = logic; this.editor = editor; this.editorActorLayer = editorActorLayer; this.initial = initial; } public void Do(ref EditorActorPreview actor) { // We can't update the ID of an EditorActorPreview in place - it's the hash and equality key of a preview. // So instead we need to swap in an entirely new preview with the updated ID. // This affects the actor layer, and the current selection. editorActorLayer.Remove(actor); actor = actor.WithId(newID); editorActorLayer.Add(actor); logic.IsChangingSelection = true; editor.DefaultBrush.SetSelection(new EditorSelection { Actor = actor }); logic.IsChangingSelection = false; } public void Undo(ref EditorActorPreview actor) { editorActorLayer.Remove(actor); actor = actor.WithId(initial); editorActorLayer.Add(actor); logic.IsChangingSelection = true; editor.DefaultBrush.SetSelection(new EditorSelection { Actor = actor }); logic.IsChangingSelection = false; } public bool IsDirty { get; private set; } public bool ShouldDoOnSave => true; } }