Merge pull request #9885 from pchote/editor-copypasta

Add copy/paste to the map editor
This commit is contained in:
Pavel Penev
2015-11-11 01:00:37 +02:00
20 changed files with 363 additions and 18 deletions

View File

@@ -24,7 +24,7 @@ using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets
{
public class EditorActorBrush : IEditorBrush
public sealed class EditorActorBrush : IEditorBrush
{
public readonly ActorInfo Actor;
@@ -148,5 +148,7 @@ namespace OpenRA.Mods.Common.Widgets
preview.Bounds.Width = (int)(zoom * s.X);
preview.Bounds.Height = (int)(zoom * s.Y);
}
public void Dispose() { }
}
}

View File

@@ -0,0 +1,174 @@
#region Copyright & License Information
/*
* Copyright 2007-2015 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. For more information,
* see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using OpenRA.FileFormats;
using OpenRA.Graphics;
using OpenRA.Mods.Common;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Orders;
using OpenRA.Primitives;
using OpenRA.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets
{
public sealed class EditorCopyPasteBrush : IEditorBrush
{
enum State { SelectFirst, SelectSecond, Paste }
readonly WorldRenderer worldRenderer;
readonly EditorViewportControllerWidget editorWidget;
readonly EditorSelectionLayer selectionLayer;
readonly EditorActorLayer editorLayer;
State state;
CPos start;
CPos end;
public EditorCopyPasteBrush(EditorViewportControllerWidget editorWidget, WorldRenderer wr)
{
this.editorWidget = editorWidget;
worldRenderer = wr;
selectionLayer = wr.World.WorldActor.Trait<EditorSelectionLayer>();
editorLayer = wr.World.WorldActor.Trait<EditorActorLayer>();
}
public bool HandleMouseInput(MouseInput mi)
{
// Exclusively uses left and right mouse buttons, but nothing else
if (mi.Button != MouseButton.Left && mi.Button != MouseButton.Right)
return false;
if (mi.Button == MouseButton.Right)
{
if (mi.Event == MouseInputEvent.Up)
{
editorWidget.ClearBrush();
return true;
}
return false;
}
if (mi.Button == MouseButton.Left && (mi.Event == MouseInputEvent.Up || mi.Event == MouseInputEvent.Down))
{
var cell = worldRenderer.Viewport.ViewToWorld(mi.Location);
switch (state)
{
case State.SelectFirst:
if (mi.Event != MouseInputEvent.Down)
break;
start = cell;
selectionLayer.SetCopyRegion(start, end);
state = State.SelectSecond;
break;
case State.SelectSecond:
if (mi.Event != MouseInputEvent.Up)
break;
end = cell;
selectionLayer.SetCopyRegion(start, end);
state = State.Paste;
break;
case State.Paste:
{
var gridType = worldRenderer.World.Map.Grid.Type;
var source = CellRegion.BoundingRegion(gridType, new[] { start, end });
Copy(source, cell - end);
editorWidget.ClearBrush();
break;
}
}
return true;
}
return false;
}
void Copy(CellRegion source, CVec offset)
{
var gridType = worldRenderer.World.Map.Grid.Type;
var mapTiles = worldRenderer.World.Map.MapTiles.Value;
var mapHeight = worldRenderer.World.Map.MapHeight.Value;
var mapResources = worldRenderer.World.Map.MapResources.Value;
var dest = new CellRegion(gridType, source.TopLeft + offset, source.BottomRight + offset);
var previews = new Dictionary<string, ActorReference>();
var tiles = new Dictionary<CPos, Tuple<TerrainTile, ResourceTile, byte>>();
foreach (var cell in source)
{
if (!mapTiles.Contains(cell) || !mapTiles.Contains(cell + offset))
continue;
tiles.Add(cell + offset, Tuple.Create(mapTiles[cell], mapResources[cell], mapHeight[cell]));
foreach (var preview in editorLayer.PreviewsAt(cell))
{
if (previews.ContainsKey(preview.ID))
continue;
var copy = preview.Export();
if (copy.InitDict.Contains<LocationInit>())
{
var location = copy.InitDict.Get<LocationInit>();
copy.InitDict.Remove(location);
copy.InitDict.Add(new LocationInit(location.Value(worldRenderer.World) + offset));
}
previews.Add(preview.ID, copy);
}
}
foreach (var kv in tiles)
{
mapTiles[kv.Key] = kv.Value.Item1;
mapResources[kv.Key] = kv.Value.Item2;
mapHeight[kv.Key] = kv.Value.Item3;
}
var removeActors = dest.SelectMany(editorLayer.PreviewsAt).Distinct().ToList();
foreach (var preview in removeActors)
editorLayer.Remove(preview);
foreach (var kv in previews)
editorLayer.Add(kv.Value);
}
public void Tick()
{
var cell = worldRenderer.Viewport.ViewToWorld(Viewport.LastMousePos);
if (state == State.Paste)
{
selectionLayer.SetPasteRegion(cell + (start - end), cell);
return;
}
if (state == State.SelectFirst)
start = end = cell;
else if (state == State.SelectSecond)
end = cell;
selectionLayer.SetCopyRegion(start, end);
}
public void Dispose()
{
selectionLayer.Clear();
}
}
}

View File

@@ -24,13 +24,13 @@ using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets
{
public interface IEditorBrush
public interface IEditorBrush : IDisposable
{
bool HandleMouseInput(MouseInput mi);
void Tick();
}
public class EditorDefaultBrush : IEditorBrush
public sealed class EditorDefaultBrush : IEditorBrush
{
public readonly ActorInfo Actor;
@@ -110,5 +110,6 @@ namespace OpenRA.Mods.Common.Widgets
}
public void Tick() { }
public void Dispose() { }
}
}

View File

@@ -24,7 +24,7 @@ using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets
{
public class EditorResourceBrush : IEditorBrush
public sealed class EditorResourceBrush : IEditorBrush
{
public readonly ResourceTypeInfo ResourceType;
@@ -119,5 +119,7 @@ namespace OpenRA.Mods.Common.Widgets
preview.Bounds.X = cellScreenPixel.X;
preview.Bounds.Y = cellScreenPixel.Y;
}
public void Dispose() { }
}
}

View File

@@ -24,7 +24,7 @@ using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets
{
public class EditorTileBrush : IEditorBrush
public sealed class EditorTileBrush : IEditorBrush
{
public readonly ushort Template;
@@ -156,5 +156,7 @@ namespace OpenRA.Mods.Common.Widgets
preview.Bounds.Width = (int)(zoom * bounds.Width);
preview.Bounds.Height = (int)(zoom * bounds.Height);
}
public void Dispose() { }
}
}

View File

@@ -708,6 +708,8 @@
<Compile Include="Lint\CheckChromeLogic.cs" />
<Compile Include="Lint\CheckMapMetadata.cs" />
<Compile Include="Widgets\Logic\MultiplayerLogic.cs" />
<Compile Include="EditorBrushes\EditorCopyPasteBrush.cs" />
<Compile Include="Traits\World\EditorSelectionLayer.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>

View File

@@ -161,5 +161,10 @@ namespace OpenRA.Mods.Common.Traits
.SelectMany(rpi => rpi.RenderPreview(init))
.ToArray();
}
public ActorReference Export()
{
return new ActorReference(actor.Type, actor.Save().ToDictionary());
}
}
}

View File

@@ -0,0 +1,105 @@
#region Copyright & License Information
/*
* Copyright 2007-2015 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. For more information,
* see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[Desc("Required for the map editor to work. Attach this to the world actor.")]
public class EditorSelectionLayerInfo : ITraitInfo
{
[PaletteReference]
[Desc("Palette to use for rendering the placement sprite.")]
public readonly string Palette = "terrain";
[Desc("Sequence image where the selection overlay types are defined.")]
public readonly string Image = "editor-overlay";
[SequenceReference("Image")]
[Desc("Sequence to use for the copy overlay.")]
public readonly string CopySequence = "copy";
[SequenceReference("Image")]
[Desc("Sequence to use for the paste overlay.")]
public readonly string PasteSequence = "paste";
public virtual object Create(ActorInitializer init) { return new EditorSelectionLayer(init.Self, this); }
}
public class EditorSelectionLayer : IWorldLoaded, IPostRender
{
readonly EditorSelectionLayerInfo info;
readonly Map map;
readonly Sprite copySprite;
readonly Sprite pasteSprite;
PaletteReference palette;
public CellRegion CopyRegion { get; private set; }
public CellRegion PasteRegion { get; private set; }
public EditorSelectionLayer(Actor self, EditorSelectionLayerInfo info)
{
if (self.World.Type != WorldType.Editor)
return;
this.info = info;
map = self.World.Map;
copySprite = map.SequenceProvider.GetSequence(info.Image, info.CopySequence).GetSprite(0);
pasteSprite = map.SequenceProvider.GetSequence(info.Image, info.PasteSequence).GetSprite(0);
}
public void WorldLoaded(World w, WorldRenderer wr)
{
if (w.Type != WorldType.Editor)
return;
palette = wr.Palette(info.Palette);
}
public void SetCopyRegion(CPos start, CPos end)
{
CopyRegion = CellRegion.BoundingRegion(map.Grid.Type, new[] { start, end });
}
public void SetPasteRegion(CPos start, CPos end)
{
PasteRegion = CellRegion.BoundingRegion(map.Grid.Type, new[] { start, end });
}
public void Clear()
{
CopyRegion = PasteRegion = null;
}
public void RenderAfterWorld(WorldRenderer wr, Actor self)
{
if (wr.World.Type != WorldType.Editor)
return;
if (CopyRegion != null)
foreach (var c in CopyRegion)
new SpriteRenderable(copySprite, wr.World.Map.CenterOfCell(c),
WVec.Zero, -511, palette, 1f, true).PrepareRender(wr).Render(wr);
if (PasteRegion != null)
foreach (var c in PasteRegion)
new SpriteRenderable(pasteSprite, wr.World.Map.CenterOfCell(c),
WVec.Zero, -511, palette, 1f, true).PrepareRender(wr).Render(wr);
}
}
}

View File

@@ -48,6 +48,9 @@ namespace OpenRA.Mods.Common.Widgets
public void ClearBrush() { SetBrush(null); }
public void SetBrush(IEditorBrush brush)
{
if (CurrentBrush != null)
CurrentBrush.Dispose();
CurrentBrush = brush ?? defaultBrush;
}

View File

@@ -21,6 +21,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
[ObjectCreator.UseCtor]
public MapEditorLogic(Widget widget, World world, WorldRenderer worldRenderer)
{
var editorViewport = widget.Get<EditorViewportControllerWidget>("MAP_EDITOR");
var gridButton = widget.GetOrNull<ButtonWidget>("GRID_BUTTON");
var terrainGeometryTrait = world.WorldActor.Trait<TerrainGeometryOverlay>();
@@ -63,6 +65,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic
};
}
var copypasteButton = widget.GetOrNull<ButtonWidget>("COPYPASTE_BUTTON");
if (copypasteButton != null)
{
copypasteButton.OnClick = () => editorViewport.SetBrush(new EditorCopyPasteBrush(editorViewport, worldRenderer));
copypasteButton.IsHighlighted = () => editorViewport.CurrentBrush is EditorCopyPasteBrush;
}
var coordinateLabel = widget.GetOrNull<LabelWidget>("COORDINATE_LABEL");
if (coordinateLabel != null)
coordinateLabel.GetText = () => worldRenderer.Viewport.ViewToWorld(Viewport.LastMousePos).ToString();

View File

@@ -362,7 +362,7 @@ Container@EDITOR_WORLD_ROOT:
Text: Actors
Font: Bold
Button@GRID_BUTTON:
X: WINDOW_RIGHT - 420
X: WINDOW_RIGHT - 500
Y: 5
Width: 100
Height: 25
@@ -373,7 +373,7 @@ Container@EDITOR_WORLD_ROOT:
TooltipText: Toggle the terrain grid
TooltipContainer: TOOLTIP_CONTAINER
Label@ZOOM_LABEL:
X: WINDOW_RIGHT - 500 - 55
X: WINDOW_RIGHT - 580 - 55
Y: 5
Width: 50
Height: 25
@@ -382,11 +382,17 @@ Container@EDITOR_WORLD_ROOT:
Font: Bold
Contrast: true
DropDownButton@ZOOM_BUTTON:
X: WINDOW_RIGHT - 500
X: WINDOW_RIGHT - 580
Y: 5
Width: 70
Height: 25
Font: Bold
Button@COPYPASTE_BUTTON:
X: WINDOW_RIGHT-390
Y: 5
Width: 96
Height: 25
Text: Copy/Paste
Label@COORDINATE_LABEL:
X: 10
Width: 50

View File

@@ -154,4 +154,5 @@ EditorWorld:
Inherits: ^BaseWorld
EditorActorLayer:
EditorResourceLayer:
EditorSelectionLayer:

View File

@@ -240,6 +240,12 @@ overlay:
target-invalid:
Start: 1
editor-overlay:
copy: overlay
Start: 0
paste: overlay
Start: 1
poweroff:
offline:
Length: *

View File

@@ -148,4 +148,5 @@ EditorWorld:
Inherits: ^BaseWorld
EditorActorLayer:
D2kEditorResourceLayer:
EditorSelectionLayer:

View File

@@ -126,19 +126,23 @@ rank:
Length: *
overlay:
build-valid-arrakis: DATA.R8
Defaults: DATA.R8
Offset: -16,-16
build-invalid: DATA.R8
build-valid-arrakis:
build-invalid:
Start: 1
Offset: -16,-16
target-select: DATA.R8
target-select:
Start: 2
Offset: -16,-16
target-valid-arrakis: DATA.R8
Offset: -16,-16
target-invalid: DATA.R8
target-valid-arrakis:
target-invalid:
Start: 1
editor-overlay:
Defaults: DATA.R8
Offset: -16,-16
copy:
paste:
Start: 1
rallypoint:
flag: flagfly.shp

View File

@@ -351,9 +351,15 @@ Container@EDITOR_WORLD_ROOT:
TooltipContainer: TOOLTIP_CONTAINER
Font: Bold
Key: escape
Button@COPYPASTE_BUTTON:
X: 170
Width: 90
Height: 25
Text: Copy/Paste
Font: Bold
Button@GRID_BUTTON:
X: 180
Width: 160
X: 270
Width: 70
Height: 25
Text: Grid
TooltipTemplate: BUTTON_TOOLTIP

View File

@@ -173,4 +173,5 @@ EditorWorld:
Inherits: ^BaseWorld
EditorActorLayer:
EditorResourceLayer:
EditorSelectionLayer:

View File

@@ -474,6 +474,12 @@ overlay:
target-invalid:
Start: 1
editor-overlay:
copy: overlay
Start: 0
paste: overlay
Start: 1
resources:
Defaults:
Length: *

View File

@@ -175,4 +175,6 @@ EditorWorld:
Inherits: ^BaseWorld
EditorActorLayer:
EditorResourceLayer:
EditorSelectionLayer:
Palette: placebuilding

View File

@@ -7,6 +7,13 @@ overlay:
Start: 1
target-select:
editor-overlay:
Defaults: place
Offset: 0, -12
copy:
paste:
Start: 1
poweroff:
offline:
Length: *