diff --git a/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs b/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs index d5cc7fc2f5..1046ffcdbb 100644 --- a/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs +++ b/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs @@ -33,6 +33,7 @@ namespace OpenRA.Mods.Common.Widgets readonly EditorViewportControllerWidget editorWidget; readonly EditorActorLayer editorLayer; readonly Dictionary resources; + public EditorActorPreview SelectedActor; int2 worldPixel; public EditorDefaultBrush(EditorViewportControllerWidget editorWidget, WorldRenderer wr) @@ -60,9 +61,10 @@ namespace OpenRA.Mods.Common.Widgets public bool HandleMouseInput(MouseInput mi) { - // Exclusively uses mouse wheel and right mouse buttons, but nothing else + // Exclusively uses mouse wheel and both mouse buttons, but nothing else // Mouse move events are important for tooltips, so we always allow these through - if ((mi.Button != MouseButton.Right && mi.Event != MouseInputEvent.Move && mi.Event != MouseInputEvent.Scroll) || + if ((mi.Button != MouseButton.Left && mi.Button != MouseButton.Right + && mi.Event != MouseInputEvent.Move && mi.Event != MouseInputEvent.Scroll) || mi.Event == MouseInputEvent.Down) return false; @@ -84,11 +86,17 @@ namespace OpenRA.Mods.Common.Widgets if (mi.Event == MouseInputEvent.Move) return false; + if (mi.Button == MouseButton.Left) + { + editorWidget.SetTooltip(null); + SelectedActor = underCursor; + } + if (mi.Button == MouseButton.Right) { editorWidget.SetTooltip(null); - if (underCursor != null) + if (underCursor != null && underCursor != SelectedActor) editorLayer.Remove(underCursor); if (mapResources.Contains(cell) && mapResources[cell].Type != 0) diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 9c77838202..d5e1c1b633 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -619,6 +619,7 @@ + diff --git a/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs b/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs index 70dd975754..19e99f878c 100644 --- a/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs +++ b/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs @@ -106,7 +106,7 @@ namespace OpenRA.Mods.Common.Traits public EditorActorPreview Add(ActorReference reference) { return Add(NextActorName(), reference); } - EditorActorPreview Add(string id, ActorReference reference, bool initialSetup = false) + public EditorActorPreview Add(string id, ActorReference reference, bool initialSetup = false) { var owner = Players.Players[reference.InitDict.Get().PlayerName]; diff --git a/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs b/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs index 13cfddcfbd..42cde20178 100644 --- a/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs +++ b/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs @@ -23,18 +23,30 @@ namespace OpenRA.Mods.Common.Traits { public class EditorActorPreview { - public readonly string Tooltip; - public readonly string ID; + public readonly string DescriptiveName; public readonly ActorInfo Info; - public readonly PlayerReference Owner; public readonly WPos CenterPosition; public readonly IReadOnlyDictionary Footprint; public readonly Rectangle Bounds; + public readonly SelectionBoxRenderable SelectionBox; + public string Tooltip + { + get + { + return (tooltip == null ? " < " + Info.Name + " >" : tooltip.Name) + "\n" + Owner.Name + " (" + Owner.Faction + ")" + + "\nID: " + ID + "\nType: " + Info.Name; + } + } + + public string ID { get; set; } + public PlayerReference Owner { get; set; } public SubCell SubCell { get; private set; } + public bool Selected { get; set; } readonly ActorReference actor; readonly WorldRenderer worldRenderer; + readonly TooltipInfoBase tooltip; IActorPreview[] previews; public EditorActorPreview(WorldRenderer worldRenderer, string id, ActorReference actor, PlayerReference owner) @@ -70,11 +82,10 @@ namespace OpenRA.Mods.Common.Traits Footprint = new ReadOnlyDictionary(footprint); } - var tooltip = Info.TraitInfos().FirstOrDefault(info => info.EnabledByDefault) as TooltipInfoBase + tooltip = Info.TraitInfos().FirstOrDefault(info => info.EnabledByDefault) as TooltipInfoBase ?? Info.TraitInfos().FirstOrDefault(info => info.EnabledByDefault); - Tooltip = (tooltip == null ? " < " + Info.Name + " >" : tooltip.Name) + "\n" + owner.Name + " (" + owner.Faction + ")" - + "\nID: " + ID + "\nType: " + Info.Name; + DescriptiveName = tooltip != null ? tooltip.Name : Info.Name; GeneratePreviews(); @@ -88,6 +99,9 @@ namespace OpenRA.Mods.Common.Traits foreach (var rr in r.Skip(1)) Bounds = Rectangle.Union(Bounds, rr); } + + SelectionBox = new SelectionBoxRenderable(new WPos(CenterPosition.X, CenterPosition.Y, 8192), + new Rectangle(Bounds.X, Bounds.Y, Bounds.Width, Bounds.Height), Color.White); } public void Tick() @@ -98,7 +112,16 @@ namespace OpenRA.Mods.Common.Traits public IEnumerable Render() { - return previews.SelectMany(p => p.Render(worldRenderer, CenterPosition)); + var items = previews.SelectMany(p => p.Render(worldRenderer, CenterPosition)); + if (Selected) + { + var highlight = worldRenderer.Palette("highlight"); + var overlay = items.Where(r => !r.IsDecoration) + .Select(r => r.WithPalette(highlight)); + return items.Concat(overlay).Append(SelectionBox); + } + + return items; } public void ReplaceInit(T init) diff --git a/OpenRA.Mods.Common/Widgets/EditorViewportControllerWidget.cs b/OpenRA.Mods.Common/Widgets/EditorViewportControllerWidget.cs index c33c2bea1e..d2199f22c6 100644 --- a/OpenRA.Mods.Common/Widgets/EditorViewportControllerWidget.cs +++ b/OpenRA.Mods.Common/Widgets/EditorViewportControllerWidget.cs @@ -21,9 +21,9 @@ namespace OpenRA.Mods.Common.Widgets public readonly string TooltipContainer; public readonly string TooltipTemplate; + public readonly EditorDefaultBrush DefaultBrush; readonly Lazy tooltipContainer; - readonly EditorDefaultBrush defaultBrush; readonly WorldRenderer worldRenderer; bool enableTooltips; @@ -33,7 +33,7 @@ namespace OpenRA.Mods.Common.Widgets { this.worldRenderer = worldRenderer; tooltipContainer = Exts.Lazy(() => Ui.Root.Get(TooltipContainer)); - CurrentBrush = defaultBrush = new EditorDefaultBrush(this, worldRenderer); + CurrentBrush = DefaultBrush = new EditorDefaultBrush(this, worldRenderer); } public void ClearBrush() { SetBrush(null); } @@ -42,7 +42,7 @@ namespace OpenRA.Mods.Common.Widgets if (CurrentBrush != null) CurrentBrush.Dispose(); - CurrentBrush = brush ?? defaultBrush; + CurrentBrush = brush ?? DefaultBrush; } public override void MouseEntered() diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorEditLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorEditLogic.cs new file mode 100644 index 0000000000..c14eb5868b --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorEditLogic.cs @@ -0,0 +1,258 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + public class ActorEditLogic : ChromeLogic + { + // 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 EditorViewportControllerWidget editor; + readonly ContainerWidget actorSelectBorder; + readonly BackgroundWidget actorEditPanel; + readonly LabelWidget typeLabel; + readonly TextFieldWidget actorIDField; + readonly LabelWidget actorIDErrorLabel; + readonly DropDownButtonWidget ownersDropDown; + readonly Widget initContainer; + readonly Widget buttonContainer; + + readonly int editPanelPadding; // Padding between right edge of actor and the edit panel. + readonly long scrollVisibleTimeout = 100; // Delay after scrolling map before edit widget becomes visible again. + long lastScrollTime = 0; + PlayerReference selectedOwner; + + ActorIDStatus actorIDStatus = ActorIDStatus.Normal; + ActorIDStatus nextActorIDStatus = ActorIDStatus.Normal; + string initialActorID; + + EditorActorPreview currentActorInner; + EditorActorPreview CurrentActor + { + get + { + return currentActorInner; + } + + set + { + if (currentActorInner == value) + return; + + if (currentActorInner != null) + currentActorInner.Selected = false; + + currentActorInner = value; + if (currentActorInner != null) + currentActorInner.Selected = true; + } + } + + [ObjectCreator.UseCtor] + public ActorEditLogic(Widget widget, World world, WorldRenderer worldRenderer, Dictionary logicArgs) + { + this.worldRenderer = worldRenderer; + editorActorLayer = world.WorldActor.Trait(); + editor = widget.Parent.Get("MAP_EDITOR"); + actorSelectBorder = editor.Get("ACTOR_SELECT_BORDER"); + actorEditPanel = editor.Get("ACTOR_EDIT_PANEL"); + + typeLabel = actorEditPanel.Get("ACTOR_TYPE_LABEL"); + actorIDField = actorEditPanel.Get("ACTOR_ID"); + + ownersDropDown = actorEditPanel.Get("OWNERS_DROPDOWN"); + + initContainer = actorEditPanel.Get("ACTOR_INIT_CONTAINER"); + buttonContainer = actorEditPanel.Get("BUTTON_CONTAINER"); + + var deleteButton = actorEditPanel.Get("DELETE_BUTTON"); + var closeButton = actorEditPanel.Get("CLOSE_BUTTON"); + + actorIDErrorLabel = actorEditPanel.Get("ACTOR_ID_ERROR_LABEL"); + actorIDErrorLabel.IsVisible = () => actorIDStatus != ActorIDStatus.Normal; + actorIDErrorLabel.GetText = () => actorIDStatus == ActorIDStatus.Duplicate ? + "Duplicate Actor ID" : "Enter an Actor ID"; + + MiniYaml yaml; + if (logicArgs.TryGetValue("EditPanelPadding", out yaml)) + editPanelPadding = FieldLoader.GetValue("EditPanelPadding", yaml.Value); + + closeButton.OnClick = Close; + deleteButton.OnClick = Delete; + actorSelectBorder.IsVisible = () => CurrentActor != null + && editor.CurrentBrush == editor.DefaultBrush + && Game.RunTime > lastScrollTime + scrollVisibleTimeout; + actorEditPanel.IsVisible = actorSelectBorder.IsVisible; + + actorIDField.OnEscKey = () => + { + actorIDField.YieldKeyboardFocus(); + return true; + }; + + actorIDField.OnTextEdited = () => + { + if (string.IsNullOrWhiteSpace(actorIDField.Text)) + { + nextActorIDStatus = ActorIDStatus.Empty; + return; + } + + // Check for duplicate actor ID + var actorId = actorIDField.Text.ToLowerInvariant(); + if (CurrentActor.ID.ToLowerInvariant() != actorId) + { + var found = world.Map.ActorDefinitions.Any(x => x.Key.ToLowerInvariant() == actorId); + if (found) + { + nextActorIDStatus = ActorIDStatus.Duplicate; + return; + } + } + + SetActorID(world, actorId); + }; + + actorIDField.OnLoseFocus = () => + { + // Reset invalid IDs back to their starting value + if (actorIDStatus != ActorIDStatus.Normal) + SetActorID(world, initialActorID); + }; + + // Setup owners drop down + selectedOwner = editorActorLayer.Players.Players.Values.First(); + Func setupItem = (option, template) => + { + var item = ScrollItemWidget.Setup(template, () => selectedOwner == option, () => + { + ownersDropDown.Text = option.Name; + ownersDropDown.TextColor = option.Color.RGB; + selectedOwner = option; + + CurrentActor.Owner = selectedOwner; + CurrentActor.ReplaceInit(new OwnerInit(selectedOwner.Name)); + }); + + item.Get("LABEL").GetText = () => option.Name; + item.GetColor = () => option.Color.RGB; + return item; + }; + + ownersDropDown.OnClick = () => + { + var owners = editorActorLayer.Players.Players.Values.OrderBy(p => p.Name); + ownersDropDown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 270, owners, setupItem); + }; + + ownersDropDown.Text = selectedOwner.Name; + ownersDropDown.TextColor = selectedOwner.Color.RGB; + } + + void SetActorID(World world, string actorId) + { + var actorDef = world.Map.ActorDefinitions.First(x => x.Key == CurrentActor.ID); + actorDef.Key = actorId; + CurrentActor.ID = actorId; + nextActorIDStatus = ActorIDStatus.Normal; + } + + public override void Tick() + { + if (actorIDStatus != nextActorIDStatus) + { + if ((actorIDStatus & nextActorIDStatus) == 0) + { + var offset = actorIDErrorLabel.Bounds.Height; + if (nextActorIDStatus == ActorIDStatus.Normal) + offset *= -1; + + actorEditPanel.Bounds.Height += offset; + initContainer.Bounds.Y += offset; + buttonContainer.Bounds.Y += offset; + } + + actorIDStatus = nextActorIDStatus; + } + + var actor = editor.DefaultBrush.SelectedActor; + if (actor != null) + { + var origin = worldRenderer.Viewport.WorldToViewPx(new int2(actor.Bounds.X, actor.Bounds.Y)); + + // If we scrolled, hide the edit box for a moment + if (actorSelectBorder.Bounds.X != origin.X || actorSelectBorder.Bounds.Y != origin.Y) + lastScrollTime = Game.RunTime; + + // If we changed actor, move widgets + if (CurrentActor != actor) + { + lastScrollTime = 0; // Ensure visible + selectedOwner = actor.Owner; + ownersDropDown.Text = selectedOwner.Name; + ownersDropDown.TextColor = selectedOwner.Color.RGB; + + CurrentActor = actor; + + initialActorID = actorIDField.Text = actor.ID; + + var font = Game.Renderer.Fonts[typeLabel.Font]; + var truncatedType = WidgetUtils.TruncateText(actor.DescriptiveName, typeLabel.Bounds.Width, font); + typeLabel.Text = truncatedType; + + actorIDField.CursorPosition = actor.ID.Length; + actorSelectBorder.Bounds.Width = actor.Bounds.Width * 2; + actorSelectBorder.Bounds.Height = actor.Bounds.Height * 2; + nextActorIDStatus = ActorIDStatus.Normal; + } + + actorSelectBorder.Bounds.X = origin.X; + actorSelectBorder.Bounds.Y = origin.Y; + + // Set the edit panel to the right of the selection border. + actorEditPanel.Bounds.X = origin.X + actorSelectBorder.Bounds.Width / 2 + editPanelPadding; + actorEditPanel.Bounds.Y = origin.Y; + } + else + { + // Selected actor is null, hide the border and edit panel. + actorIDField.YieldKeyboardFocus(); + CurrentActor = null; + } + } + + void Delete() + { + if (CurrentActor != null) + editorActorLayer.Remove(CurrentActor); + + Close(); + } + + void Close() + { + actorIDField.YieldKeyboardFocus(); + editor.DefaultBrush.SelectedActor = null; + actorSelectBorder.Visible = false; + CurrentActor = null; + } + } +} diff --git a/mods/cnc/chrome/editor.yaml b/mods/cnc/chrome/editor.yaml index e2795f372c..1940b84816 100644 --- a/mods/cnc/chrome/editor.yaml +++ b/mods/cnc/chrome/editor.yaml @@ -207,8 +207,9 @@ Container@EDITOR_ROOT: TooltipContainer@TOOLTIP_CONTAINER: Container@EDITOR_WORLD_ROOT: - Logic: LoadIngamePerfLogic, MapEditorLogic + Logic: LoadIngamePerfLogic, MapEditorLogic, ActorEditLogic ChangeZoomKey: TogglePixelDouble + EditPanelPadding: 5 Children: Container@PERF_ROOT: EditorViewportController@MAP_EDITOR: @@ -223,6 +224,71 @@ Container@EDITOR_WORLD_ROOT: Visible: false ActorPreview@DRAG_ACTOR_PREVIEW: Visible: false + Container@ACTOR_SELECT_BORDER: + X: 32 + Y: 32 + Width: 32 + Height: 32 + Background@ACTOR_EDIT_PANEL: + Background: panel-black + Width: 269 + Height: 114 + Children: + Label@ACTOR_TYPE_LABEL: + X: 2 + Y: 1 + Width: 265 + Height: 24 + Align: Center + Font: Bold + Label@ACTOR_ID_LABEL: + X: 0 + Y: 29 + Width: 55 + Height: 24 + Text: ID + Align: Right + TextField@ACTOR_ID: + X: 65 + Y: 29 + Width: 200 + Height: 25 + Label@ACTOR_ID_ERROR_LABEL: + X: 65 + Y: 54 + Width: 260 + Height: 15 + Font: TinyBold + TextColor: FF0000 + Container@ACTOR_INIT_CONTAINER: + Y: 57 + Width: PARENT_RIGHT + Children: + Label@OWNERS_LABEL: + Width: 55 + Height: 24 + Text: Owner + Align: Right + DropDownButton@OWNERS_DROPDOWN: + X: 65 + Width: 200 + Height: 25 + Font: Bold + Container@BUTTON_CONTAINER: + Y: 85 + Children: + Button@DELETE_BUTTON: + X: 4 + Width: 85 + Height: 25 + Text: Delete + Font: Bold + Button@CLOSE_BUTTON: + X: 180 + Width: 85 + Height: 25 + Text: Close + Font: Bold ViewportController: Width: WINDOW_RIGHT Height: WINDOW_BOTTOM diff --git a/mods/common/chrome/editor.yaml b/mods/common/chrome/editor.yaml index c82101a85a..22ea471c0e 100644 --- a/mods/common/chrome/editor.yaml +++ b/mods/common/chrome/editor.yaml @@ -198,8 +198,9 @@ Container@EDITOR_ROOT: TooltipContainer@TOOLTIP_CONTAINER: Container@EDITOR_WORLD_ROOT: - Logic: LoadIngamePerfLogic, MapEditorLogic + Logic: LoadIngamePerfLogic, MapEditorLogic, ActorEditLogic ChangeZoomKey: TogglePixelDouble + EditPanelPadding: 14 Children: Container@PERF_ROOT: EditorViewportController@MAP_EDITOR: @@ -214,6 +215,73 @@ Container@EDITOR_WORLD_ROOT: Visible: false Sprite@DRAG_LAYER_PREVIEW: Visible: false + Container@ACTOR_SELECT_BORDER: + X: 32 + Y: 32 + Width: 32 + Height: 32 + Background@ACTOR_EDIT_PANEL: + X: 32 + Y: 32 + Width: 294 + Height: 144 + Children: + Label@ACTOR_TYPE_LABEL: + X: 15 + Y: 15 + Width: 265 + Height: 24 + Align: Center + Font: Bold + Label@ACTOR_ID_LABEL: + X: 15 + Y: 45 + Width: 55 + Height: 24 + Text: ID + Align: Right + TextField@ACTOR_ID: + X: 80 + Y: 45 + Width: 200 + Height: 25 + Label@ACTOR_ID_ERROR_LABEL: + X: 80 + Y: 70 + Width: 260 + Height: 15 + Font: TinyBold + TextColor: FF0000 + Container@ACTOR_INIT_CONTAINER: + Y: 75 + Width: PARENT_RIGHT + Children: + Label@OWNERS_LABEL: + X: 15 + Width: 55 + Height: 24 + Text: Owner + Align: Right + DropDownButton@OWNERS_DROPDOWN: + X: 80 + Width: 200 + Height: 25 + Font: Bold + Container@BUTTON_CONTAINER: + Y: 105 + Children: + Button@DELETE_BUTTON: + X: 15 + Width: 85 + Height: 25 + Text: Delete + Font: Bold + Button@CLOSE_BUTTON: + X: 195 + Width: 85 + Height: 25 + Text: Close + Font: Bold ViewportController: Width: WINDOW_RIGHT Height: WINDOW_BOTTOM