Jelajahi Sumber

Merge pull request #162 from PixiEditor/contextMenus

Added canvas context menu and duplicate layer command
Krzysztof Krysiński 4 tahun lalu
induk
melakukan
c9eaa70c75

+ 13 - 0
PixiEditor/Helpers/StringExtensions.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Linq;
+
+namespace PixiEditor.Helpers
+{
+    public static class StringExtensions
+    {
+        public static string Reverse(this string s)
+        {
+            return new string(s.Reverse<char>().ToArray());
+        }
+    }
+}

+ 99 - 1
PixiEditor/Models/DataHolders/Document/Document.Layers.cs

@@ -2,9 +2,11 @@
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
+using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows.Media.Imaging;
 using GalaSoft.MvvmLight.Messaging;
+using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Layers;
@@ -17,6 +19,7 @@ namespace PixiEditor.Models.DataHolders
     {
         public const string MainSelectedLayerColor = "#505056";
         public const string SecondarySelectedLayerColor = "#7D505056";
+        private static readonly Regex reversedLayerSuffixRegex = new (@"(?:\)([0-9]+)*\()? *([\s\S]+)", RegexOptions.Compiled);
         private Guid activeLayerGuid;
 
         public ObservableCollection<Layer> Layers { get; set; } = new ObservableCollection<Layer>();
@@ -96,11 +99,16 @@ namespace PixiEditor.Models.DataHolders
 
         public void AddNewLayer(string name, int width, int height, bool setAsActive = true)
         {
-            Layers.Add(new Layer(name, width, height)
+            Layer layer;
+
+            Layers.Add(layer = new Layer(name, width, height)
             {
                 MaxHeight = Height,
                 MaxWidth = Width
             });
+
+            layer.Name = GetLayerSuffix(layer);
+
             if (setAsActive)
             {
                 SetMainActiveLayer(Layers.Count - 1);
@@ -120,6 +128,31 @@ namespace PixiEditor.Models.DataHolders
             LayersChanged?.Invoke(this, new LayersChangedEventArgs(Layers[0].LayerGuid, LayerAction.Add));
         }
 
+        /// <summary>
+        /// Duplicates the layer at the <paramref name="index"/>.
+        /// </summary>
+        /// <param name="index">The index of the layer to duplicate.</param>
+        /// <returns>The duplicate.</returns>
+        public Layer DuplicateLayer(int index)
+        {
+            Layer duplicate = Layers[index].Clone(true);
+
+            duplicate.Name = GetLayerSuffix(duplicate);
+
+            Layers.Insert(index + 1, duplicate);
+            SetMainActiveLayer(index + 1);
+
+            StorageBasedChange storageChange = new (this, new[] { duplicate }, false);
+            UndoManager.AddUndoChange(
+                storageChange.ToChange(
+                    RemoveLayerProcess,
+                    new object[] { duplicate.LayerGuid },
+                    RestoreLayersProcess,
+                    "Duplicate Layer"));
+
+            return duplicate;
+        }
+
         public void SetNextLayerAsActive(int lastLayerIndex)
         {
             if (Layers.Count > 0)
@@ -437,6 +470,71 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
+        /// <summary>
+        /// Get's the layers suffix, e.g. "New Layer" becomes "New Layer (1)".
+        /// </summary>
+        /// <returns>Name of the layer with suffix.</returns>
+        private string GetLayerSuffix(Layer layer)
+        {
+            Match match = reversedLayerSuffixRegex.Match(layer.Name.Reverse());
+
+            int? highestValue = GetHighestSuffix(layer, match.Groups[2].Value, reversedLayerSuffixRegex);
+
+            string actualName = match.Groups[2].Value.Reverse();
+
+            if (highestValue == null)
+            {
+                return actualName;
+            }
+
+            return actualName + $" ({highestValue + 1})";
+        }
+
+        private int? GetHighestSuffix(Layer except, string layerName, Regex regex)
+        {
+            int? highestValue = null;
+
+            foreach (Layer otherLayer in Layers)
+            {
+                if (otherLayer == except)
+                {
+                    continue;
+                }
+
+                Match otherMatch = regex.Match(otherLayer.Name.Reverse());
+
+                if (otherMatch.Groups[2].Value == layerName)
+                {
+                    SetHighest(otherMatch.Groups[1].Value.Reverse(), ref highestValue);
+                }
+            }
+
+            return highestValue;
+        }
+
+        /// <returns>Was the parse a sucess.</returns>
+        private bool SetHighest(string number, ref int? highest, int? defaultValue = 0)
+        {
+            bool sucess = int.TryParse(number, out int parsedNumber);
+
+            if (sucess)
+            {
+                if (highest == null || highest < parsedNumber)
+                {
+                    highest = parsedNumber;
+                }
+            }
+            else
+            {
+                if (highest == null)
+                {
+                    highest = defaultValue;
+                }
+            }
+
+            return sucess;
+        }
+
         private void RemoveLayersProcess(object[] parameters)
         {
             if (parameters != null && parameters.Length > 0 && parameters[0] is IEnumerable<Guid> layerGuids)

+ 14 - 1
PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -15,6 +15,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public RelayCommand DeleteLayersCommand { get; set; }
 
+        public RelayCommand DuplicateLayerCommand { get; set; }
+
         public RelayCommand RenameLayerCommand { get; set; }
 
         public RelayCommand MoveToBackCommand { get; set; }
@@ -33,6 +35,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             SetActiveLayerCommand = new RelayCommand(SetActiveLayer);
             NewLayerCommand = new RelayCommand(NewLayer, CanCreateNewLayer);
             DeleteLayersCommand = new RelayCommand(DeleteLayer, CanDeleteLayer);
+            DuplicateLayerCommand = new RelayCommand(DuplicateLayer, CanDuplicateLayer);
             MoveToBackCommand = new RelayCommand(MoveLayerToBack, CanMoveToBack);
             MoveToFrontCommand = new RelayCommand(MoveLayerToFront, CanMoveToFront);
             RenameLayerCommand = new RelayCommand(RenameLayer);
@@ -49,7 +52,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public void NewLayer(object parameter)
         {
-            Owner.BitmapManager.ActiveDocument.AddNewLayer($"New Layer {Owner.BitmapManager.ActiveDocument.Layers.Count}");
+            Owner.BitmapManager.ActiveDocument.AddNewLayer($"New Layer");
         }
 
         public bool CanCreateNewLayer(object parameter)
@@ -98,6 +101,16 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             return Owner.BitmapManager.ActiveDocument != null && Owner.BitmapManager.ActiveDocument.Layers.Count > 1;
         }
 
+        public void DuplicateLayer(object parameter)
+        {
+            Owner.BitmapManager.ActiveDocument.DuplicateLayer((int)parameter);
+        }
+
+        public bool CanDuplicateLayer(object property)
+        {
+            return Owner.BitmapManager.ActiveDocument != null;
+        }
+
         public void RenameLayer(object parameter)
         {
             int? index = (int?)parameter;

+ 15 - 0
PixiEditor/Views/MainWindow.xaml

@@ -183,6 +183,16 @@
             </ItemsControl>
         </StackPanel>
         <Grid Grid.Column="1" Grid.Row="2" Background="#303030">
+            <Grid.ContextMenu>
+                <ContextMenu>
+                    <MenuItem Header="_Select All" Command="{Binding SelectionSubViewModel.SelectAllCommand}" InputGestureText="Ctrl+A" />
+                    <MenuItem Header="_Deselect" Command="{Binding SelectionSubViewModel.DeselectCommand}" InputGestureText="Ctrl+D" />
+                    <Separator/>
+                    <MenuItem Header="_Cut" Command="{Binding ClipboardSubViewModel.CutCommand}" InputGestureText="Ctrl+X" />
+                    <MenuItem Header="_Copy" Command="{Binding ClipboardSubViewModel.CopyCommand}" InputGestureText="Ctrl+C" />
+                    <MenuItem Header="_Paste" Command="{Binding ClipboardSubViewModel.PasteCommand}" InputGestureText="Ctrl+V" />
+                </ContextMenu>
+            </Grid.ContextMenu>
             <Grid>
                 <DockingManager ActiveContent="{Binding BitmapManager.ActiveDocument, Mode=TwoWay}" 
                                            DocumentsSource="{Binding BitmapManager.Documents}">
@@ -344,6 +354,11 @@
                                                                                     RelativeSource={RelativeSource AncestorType=ContextMenu}}"
 
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
+                                Path=(ItemsControl.AlternationIndex)}" />
+                                                                        <MenuItem Header="Duplicate"
+                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.DuplicateLayerCommand,
+                                                                                    RelativeSource={RelativeSource AncestorType=ContextMenu}}"
+                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                 Path=(ItemsControl.AlternationIndex)}" />
                                                                         <MenuItem Header="Rename"
                                                                                   Command="{Binding PlacementTarget.Tag.LayersSubViewModel.RenameLayerCommand,

+ 37 - 0
PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs

@@ -3,6 +3,7 @@ using System.Windows.Media;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
+using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.ViewModels;
 using Xunit;
@@ -306,5 +307,41 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
 
             Assert.Contains(viewModel.FileSubViewModel.RecentlyOpened, x => x == testFilePath);
         }
+
+        [Fact]
+        public void TestThatDupliacteLayerWorks()
+        {
+            const string layerName = "New Layer";
+
+            Document document = new (10, 10);
+
+            document.AddNewLayer(layerName);
+            Layer duplicate = document.DuplicateLayer(0);
+
+            Assert.Equal(document.Layers[1], duplicate);
+            Assert.Equal(layerName + " (1)", duplicate.Name);
+            Assert.True(duplicate.IsActive);
+        }
+
+        [Fact]
+        public void TestThatCorrectLayerSuffixIsSet()
+        {
+            const string layerName = "New Layer";
+
+            Document document = new (10, 10);
+
+            document.AddNewLayer(layerName);
+            document.AddNewLayer(layerName);
+            document.AddNewLayer(layerName);
+
+            Assert.Equal(layerName, document.Layers[0].Name);
+            Assert.Equal(layerName + " (1)", document.Layers[1].Name);
+            Assert.Equal(layerName + " (2)", document.Layers[2].Name);
+
+            document.Layers.Add(new Layer(layerName + " (15)"));
+            document.AddNewLayer(layerName);
+
+            Assert.Equal(layerName + " (16)", document.Layers[4].Name);
+        }
     }
 }