2
0
Эх сурвалжийг харах

Merge pull request #442 from PixiEditor/bugs-n-improvements

Bugs and Improvements
Krzysztof Krysiński 2 жил өмнө
parent
commit
1283cbd42b
36 өөрчлөгдсөн 995 нэмэгдсэн , 205 устгасан
  1. 11 0
      src/PixiEditor.ChangeableDocument/Enums/FlipType.cs
  2. 20 1
      src/PixiEditor/Helpers/Behaviours/SliderUpdateBehavior.cs
  3. 39 0
      src/PixiEditor/Helpers/Collections/ActionDisplayList.cs
  4. 39 0
      src/PixiEditor/Helpers/ColorHelper.cs
  5. 27 0
      src/PixiEditor/Helpers/Converters/EmptyStringFillerConverter.cs
  6. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180Deg.png
  7. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180DegLayers.png
  8. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270Deg.png
  9. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270DegLayers.png
  10. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate90Deg.png
  11. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate90DegLayers.png
  12. 12 6
      src/PixiEditor/Models/Commands/CommandController.cs
  13. 34 23
      src/PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs
  14. 38 12
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  15. 2 1
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  16. 168 3
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  17. 27 13
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs
  18. 12 0
      src/PixiEditor/PixiEditor.csproj
  19. 11 42
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs
  20. 2 2
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs
  21. 16 0
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  22. 73 35
      src/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs
  23. 17 0
      src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs
  24. 11 5
      src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  25. 54 5
      src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  26. 5 32
      src/PixiEditor/ViewModels/ViewModelMain.cs
  27. 83 0
      src/PixiEditor/Views/Dialogs/CommandDebugPopup.xaml
  28. 142 0
      src/PixiEditor/Views/Dialogs/CommandDebugPopup.xaml.cs
  29. 11 3
      src/PixiEditor/Views/MainWindow.xaml
  30. 40 7
      src/PixiEditor/Views/MainWindow.xaml.cs
  31. 1 0
      src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml
  32. 44 3
      src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml.cs
  33. 2 1
      src/PixiEditor/Views/UserControls/Layers/ReferenceLayer.xaml
  34. 33 0
      src/PixiEditor/Views/UserControls/Layers/ReferenceLayer.xaml.cs
  35. 19 8
      src/PixiEditor/Views/UserControls/Palettes/PaletteViewer.xaml.cs
  36. 2 3
      src/PixiEditor/Views/UserControls/PreviewWindow.xaml.cs

+ 11 - 0
src/PixiEditor.ChangeableDocument/Enums/FlipType.cs

@@ -11,7 +11,18 @@ public enum FlipType
 /// </summary>
 public enum RotationAngle
 {
+    /// <summary>
+    /// 90 Degree
+    /// </summary>
     D90,
+    
+    /// <summary>
+    /// 180 Degree
+    /// </summary>
     D180,
+
+    /// <summary>
+    /// -90 Degree
+    /// </summary>
     D270
 }

+ 20 - 1
src/PixiEditor/Helpers/Behaviours/SliderUpdateBehavior.cs

@@ -3,7 +3,6 @@ using System.Windows.Controls;
 using System.Windows.Controls.Primitives;
 using System.Windows.Input;
 using Microsoft.Xaml.Behaviors;
-
 namespace PixiEditor.Helpers.Behaviours;
 #nullable enable
 internal class SliderUpdateBehavior : Behavior<Slider>
@@ -41,6 +40,15 @@ internal class SliderUpdateBehavior : Behavior<Slider>
         set => SetValue(DragStartedProperty, value);
     }
 
+    public static readonly DependencyProperty SetOpacityProperty =
+        DependencyProperty.Register(nameof(SetOpacity), typeof(ICommand), typeof(SliderUpdateBehavior), new(null));
+
+    public ICommand SetOpacity
+    {
+        get => (ICommand)GetValue(SetOpacityProperty);
+        set => SetValue(SetOpacityProperty, value);
+    }
+
     public static DependencyProperty ValueFromSliderProperty =
         DependencyProperty.Register(nameof(ValueFromSlider), typeof(double), typeof(SliderUpdateBehavior), new(OnSliderValuePropertyChange));
     public double ValueFromSlider
@@ -55,6 +63,8 @@ internal class SliderUpdateBehavior : Behavior<Slider>
     private bool bindingValueChangedWhileDragging = false;
     private double bindingValueWhileDragging = 0.0;
 
+    private bool skipSetOpacity;
+    
     protected override void OnAttached()
     {
         AssociatedObject.Loaded += AssociatedObject_Loaded;
@@ -101,23 +111,32 @@ internal class SliderUpdateBehavior : Behavior<Slider>
     private static void OnSliderValuePropertyChange(DependencyObject slider, DependencyPropertyChangedEventArgs e)
     {
         SliderUpdateBehavior obj = (SliderUpdateBehavior)slider;
+        
         if (obj.dragging)
         {
             if (obj.DragValueChanged is not null && obj.DragValueChanged.CanExecute(e.NewValue))
                 obj.DragValueChanged.Execute(e.NewValue);
         }
+        else if (!obj.skipSetOpacity)
+        {
+            if (obj.SetOpacity is not null && obj.SetOpacity.CanExecute(e.NewValue))
+                obj.SetOpacity.Execute(e.NewValue);
+        }
     }
 
     private static void OnBindingValuePropertyChange(DependencyObject slider, DependencyPropertyChangedEventArgs e)
     {
         SliderUpdateBehavior obj = (SliderUpdateBehavior)slider;
+        obj.skipSetOpacity = true;
         if (obj.dragging)
         {
             obj.bindingValueChangedWhileDragging = true;
             obj.bindingValueWhileDragging = (double)e.NewValue;
+            obj.skipSetOpacity = false;
             return;
         }
         obj.ValueFromSlider = (double)e.NewValue;
+        obj.skipSetOpacity = false;
     }
 
     private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)

+ 39 - 0
src/PixiEditor/Helpers/Collections/ActionDisplayList.cs

@@ -0,0 +1,39 @@
+using System.Collections;
+
+namespace PixiEditor.Helpers.Collections;
+
+public class ActionDisplayList : IEnumerable<KeyValuePair<string, string>>
+{
+    private Dictionary<string, string> _dictionary = new();
+    private Action notifyUpdate;
+
+    public ActionDisplayList(Action notifyUpdate)
+    {
+        this.notifyUpdate = notifyUpdate;
+    }
+
+    public string this[string key]
+    {
+        get => _dictionary[key];
+        set
+        {
+            if (value == null)
+            {
+                _dictionary.Remove(key);
+                notifyUpdate();
+                return;
+            }
+            
+            _dictionary[key] = value;
+            notifyUpdate();
+        }
+    }
+
+    public string GetActive() => _dictionary.Last().Value;
+
+    public bool HasActive() => _dictionary.Count != 0;
+
+    public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => _dictionary.GetEnumerator();
+
+    IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator();
+}

+ 39 - 0
src/PixiEditor/Helpers/ColorHelper.cs

@@ -0,0 +1,39 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+using System.Windows;
+
+namespace PixiEditor.Helpers;
+
+public class ColorHelper
+{
+    public static bool ParseAnyFormat(IDataObject data, [NotNullWhen(true)] out DrawingApi.Core.ColorsImpl.Color? result) => 
+        ParseAnyFormat(((DataObject)data).GetText().Trim(), out result);
+    
+    public static bool ParseAnyFormat(string value, [NotNullWhen(true)] out DrawingApi.Core.ColorsImpl.Color? result)
+    {
+        bool hex = Regex.IsMatch(value, "^#?([a-fA-F0-9]{8}|[a-fA-F0-9]{6}|[a-fA-F0-9]{3})$");
+
+        if (hex)
+        {
+            result = DrawingApi.Core.ColorsImpl.Color.Parse(value);
+            return true;
+        }
+
+        var match = Regex.Match(value, @"(?:rgba?\(?)? *(?<r>\d{1,3})(?:, *| +)(?<g>\d{1,3})(?:, *| +)(?<b>\d{1,3})(?:(?:, *| +)(?<a>\d{0,3}))?\)?");
+
+        if (!match.Success)
+        {
+            result = null;
+            return false;
+        }
+
+        byte r = byte.Parse(match.Groups["r"].ValueSpan);
+        byte g = byte.Parse(match.Groups["g"].ValueSpan);
+        byte b = byte.Parse(match.Groups["b"].ValueSpan);
+        byte a = match.Groups["a"].Success ? byte.Parse(match.Groups["a"].ValueSpan) : (byte)255;
+
+        result = new DrawingApi.Core.ColorsImpl.Color(r, g, b, a);
+        return true;
+
+    }
+}

+ 27 - 0
src/PixiEditor/Helpers/Converters/EmptyStringFillerConverter.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers.Converters;
+internal class EmptyStringFillerConverter : MarkupConverter
+{
+    public string NullText { get; set; } = "[null]";
+
+    public string EmptyText { get; set; } = "[empty]";
+
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return value switch
+        {
+            string s => s.Length switch
+            {
+                0 => EmptyText,
+                _ => s
+            },
+            _ => NullText
+        };
+    }
+}

BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180Deg.png


BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180DegLayers.png


BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270Deg.png


BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270DegLayers.png


BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate90Deg.png


BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate90DegLayers.png


+ 12 - 6
src/PixiEditor/Models/Commands/CommandController.cs

@@ -115,16 +115,22 @@ internal class CommandController
         LoadCommands(serviceProvider, compiledCommandList, commandGroupsData, commands, template);
         LoadTools(serviceProvider, commandGroupsData, commands, template);
 
+        var miscList = new List<Command>();
+
         foreach (var (groupInternalName, storedCommands) in commands)
         {
             var groupData = commandGroupsData.FirstOrDefault(group => group.internalName == groupInternalName);
-            string groupDisplayName;
-            if (groupData == default)
-                groupDisplayName = "Misc";
-            else
-                groupDisplayName = groupData.displayName;
-            CommandGroups.Add(new(groupDisplayName, storedCommands));
+            if (groupData == default || groupData.internalName == "PixiEditor.Links")
+            {
+                miscList.AddRange(storedCommands);
+                continue;
+            }
+
+            string groupDisplayName = groupData.displayName;
+            CommandGroups.Add(new CommandGroup(groupDisplayName, storedCommands));
         }
+        
+        CommandGroups.Add(new CommandGroup("Misc", miscList));
     }
 
     private void LoadTools(IServiceProvider serviceProvider, List<(string internalName, string displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands,

+ 34 - 23
src/PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs

@@ -14,39 +14,50 @@ internal class IconEvaluator : Evaluator<ImageSource>
     public override ImageSource CallEvaluate(Command command, object parameter) =>
         base.CallEvaluate(command, parameter ?? command);
 
-    [DebuggerDisplay("IconEvaluator.Default")]
-    private class CommandNameEvaluator : IconEvaluator
+    public static string GetDefaultPath(Command command)
     {
-        public static string[] resources = GetResourceNames();
-
-        public static Dictionary<string, BitmapImage> images = new();
+        string path;
 
-        public override ImageSource CallEvaluate(Command command, object parameter)
+        if (command.IconPath != null)
         {
-            string path;
-
-            if (command.IconPath != null)
+            if (command.IconPath.StartsWith('@'))
             {
-                if (command.IconPath.StartsWith('@'))
-                {
-                    path = command.IconPath[1..];
-                }
-                else
-                {
-                    path = $"Images/{command.IconPath}";
-                }
+                path = command.IconPath[1..];
+            }
+            else if (command.IconPath.StartsWith('$'))
+            {
+                path = $"Images/Commands/{command.IconPath[1..].Replace('.', '/')}.png";
             }
             else
             {
-                path = $"Images/Commands/{command.InternalName.Replace('.', '/')}.png";
+                path = $"Images/{command.IconPath}";
             }
+        }
+        else
+        {
+            path = $"Images/Commands/{command.InternalName.Replace('.', '/')}.png";
+        }
 
-            path = path.ToLower();
+        path = path.ToLower();
 
-            if (path.StartsWith("/"))
-            {
-                path = path[1..];
-            }
+        if (path.StartsWith("/"))
+        {
+            path = path[1..];
+        }
+
+        return path;
+    }
+
+    [DebuggerDisplay("IconEvaluator.Default")]
+    private class CommandNameEvaluator : IconEvaluator
+    {
+        public static string[] resources = GetResourceNames();
+
+        public static Dictionary<string, BitmapImage> images = new();
+
+        public override ImageSource CallEvaluate(Command command, object parameter)
+        {
+            string path = GetDefaultPath(command);
 
             if (resources.Contains(path))
             {

+ 38 - 12
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -7,6 +7,7 @@ using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.Helpers;
@@ -68,15 +69,31 @@ internal static class ClipboardController
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     /// </summary>
-    public static bool TryPasteFromClipboard(DocumentViewModel document)
+    public static bool TryPaste(DocumentViewModel document, DataObject data, bool pasteAsNew = false)
     {
-        List<(string? name, Surface image)> images = GetImagesFromClipboard();
+        List<(string? name, Surface image)> images = GetImage(data);
         if (images.Count == 0)
             return false;
 
         if (images.Count == 1)
         {
-            document.Operations.PasteImageWithTransform(images[0].image, VecI.Zero);
+            if (pasteAsNew)
+            {
+                var guid = document.Operations.CreateStructureMember(StructureMemberType.Layer, "New Layer", false);
+
+                if (guid == null)
+                {
+                    return false;
+                }
+                
+                document.Operations.SetSelectedMember(guid.Value);
+                document.Operations.PasteImageWithTransform(images[0].image, VecI.Zero, guid.Value, false);
+            }
+            else
+            {
+                document.Operations.PasteImageWithTransform(images[0].image, VecI.Zero);
+            }
+            
             return true;
         }
 
@@ -84,12 +101,19 @@ internal static class ClipboardController
         return true;
     }
 
+    /// <summary>
+    ///     Pastes image from clipboard into new layer.
+    /// </summary>
+    public static bool TryPasteFromClipboard(DocumentViewModel document, bool pasteAsNew = false) =>
+        TryPaste(document, ClipboardHelper.TryGetDataObject(), pasteAsNew);
+
+    public static List<(string? name, Surface image)> GetImagesFromClipboard() => GetImage(ClipboardHelper.TryGetDataObject());
+
     /// <summary>
     /// Gets images from clipboard, supported PNG, Dib and Bitmap.
     /// </summary>
-    private static List<(string? name, Surface image)> GetImagesFromClipboard()
+    public static List<(string? name, Surface image)> GetImage(DataObject? data)
     {
-        DataObject data = ClipboardHelper.TryGetDataObject();
         List<(string? name, Surface image)> surfaces = new();
 
         if (data == null)
@@ -121,15 +145,16 @@ internal static class ClipboardController
         return surfaces;
     }
 
-    public static bool IsImageInClipboard()
+    public static bool IsImageInClipboard() => IsImage(ClipboardHelper.TryGetDataObject());
+    
+    public static bool IsImage(DataObject? dataObject)
     {
-        DataObject dao = ClipboardHelper.TryGetDataObject();
-        if (dao == null)
+        if (dataObject == null)
             return false;
 
         try
         {
-            var files = dao.GetFileDropList();
+            var files = dataObject.GetFileDropList();
             if (files != null)
             {
                 foreach (var file in files)
@@ -146,8 +171,7 @@ internal static class ClipboardController
             return false;
         }
 
-        return dao.GetDataPresent("PNG") || dao.GetDataPresent(DataFormats.Dib) ||
-               dao.GetDataPresent(DataFormats.Bitmap) || dao.GetDataPresent(DataFormats.FileDrop);
+        return HasData(dataObject, "PNG", DataFormats.Dib, DataFormats.Bitmap);
     }
 
     private static BitmapSource FromPNG(DataObject data)
@@ -158,6 +182,8 @@ internal static class ClipboardController
         return decoder.Frames[0];
     }
 
+    private static bool HasData(DataObject dataObject, params string[] formats) => formats.Any(dataObject.GetDataPresent);
+    
     private static bool TryExtractSingleImage(DataObject data, [NotNullWhen(true)] out Surface? result)
     {
         try
@@ -168,7 +194,7 @@ internal static class ClipboardController
             {
                 source = FromPNG(data);
             }
-            else if (data.GetDataPresent(DataFormats.Dib) || data.GetDataPresent(DataFormats.Bitmap))
+            else if (HasData(data, DataFormats.Dib, DataFormats.Bitmap))
             {
                 source = Clipboard.GetImage();
             }

+ 2 - 1
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -378,12 +378,13 @@ internal class DocumentUpdater
                 ProcessCreateStructureMember(childInfo);
             }
         }
-
+        
         if (doc.SelectedStructureMember is not null)
         {
             doc.SelectedStructureMember.Selection = StructureMemberSelectionType.None;
             doc.SelectedStructureMember.RaisePropertyChanged(nameof(doc.SelectedStructureMember.Selection));
         }
+        
         doc.InternalSetSelectedMember(memberVM);
         memberVM.Selection = StructureMemberSelectionType.Hard;
         doc.RaisePropertyChanged(nameof(doc.SelectedStructureMember));

+ 168 - 3
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -24,6 +24,9 @@ internal class DocumentOperationsModule
         Internals = internals;
     }
 
+    /// <summary>
+    /// Creates a new selection with the size of the document
+    /// </summary>
     public void SelectAll()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -33,6 +36,9 @@ internal class DocumentOperationsModule
             new EndSelectRectangle_Action());
     }
 
+    /// <summary>
+    /// Clears the current selection
+    /// </summary>
     public void ClearSelection()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -40,6 +46,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClearSelection_Action());
     }
 
+    /// <summary>
+    /// Deletes selected pixels
+    /// </summary>
+    /// <param name="clearSelection">Should the selection be cleared</param>
     public void DeleteSelectedPixels(bool clearSelection = false)
     {
         var member = Document.SelectedStructureMember;
@@ -54,6 +64,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions();
     }
 
+    /// <summary>
+    /// Sets the opacity of the member with the guid <paramref name="memberGuid"/>
+    /// </summary>
+    /// <param name="memberGuid">The Guid of the member</param>
+    /// <param name="value">A value between 0 and 1</param>
     public void SetMemberOpacity(Guid memberGuid, float value)
     {
         if (Internals.ChangeController.IsChangeActive || value is > 1 or < 0)
@@ -63,10 +78,20 @@ internal class DocumentOperationsModule
             new EndStructureMemberOpacity_Action());
     }
 
+    /// <summary>
+    /// Adds a new viewport or updates a existing one
+    /// </summary>
     public void AddOrUpdateViewport(ViewportInfo info) => Internals.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(info));
 
+    /// <summary>
+    /// Deletes the viewport with the <paramref name="viewportGuid"/>
+    /// </summary>
+    /// <param name="viewportGuid">The Guid of the viewport to remove</param>
     public void RemoveViewport(Guid viewportGuid) => Internals.ActionAccumulator.AddActions(new RemoveViewport_PassthroughAction(viewportGuid));
 
+    /// <summary>
+    /// Delete the whole undo stack
+    /// </summary>
     public void ClearUndo()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -74,6 +99,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddActions(new DeleteRecordedChanges_Action());
     }
 
+    /// <summary>
+    /// Pastes the <paramref name="images"/> as new layers
+    /// </summary>
+    /// <param name="images">The images to paste</param>
     public void PasteImagesAsLayers(List<(string? name, Surface image)> images)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -90,19 +119,29 @@ internal class DocumentOperationsModule
 
         foreach (var imageWithName in images)
         {
-            var layerGuid = Internals.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer, imageWithName.name, true);
+            var layerGuid = Internals.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer, imageWithName.name);
             DrawImage(imageWithName.image, new ShapeCorners(new RectD(VecD.Zero, imageWithName.image.Size)), layerGuid, true, false, false);
         }
         Internals.ActionAccumulator.AddFinishedActions();
     }
 
-    public Guid? CreateStructureMember(StructureMemberType type, string? name = null)
+    /// <summary>
+    /// Creates a new structure member of type <paramref name="type"/> with the name <paramref name="name"/>
+    /// </summary>
+    /// <param name="type">The type of the member</param>
+    /// <param name="name">The name of the member</param>
+    /// <returns>The Guid of the new structure member or null if there is already an active change</returns>
+    public Guid? CreateStructureMember(StructureMemberType type, string? name = null, bool finish = true)
     {
         if (Internals.ChangeController.IsChangeActive)
             return null;
-        return Internals.StructureHelper.CreateNewStructureMember(type, name, true);
+        return Internals.StructureHelper.CreateNewStructureMember(type, name, finish);
     }
 
+    /// <summary>
+    /// Duplicates the layer with the <paramref name="guidValue"/>
+    /// </summary>
+    /// <param name="guidValue">The Guid of the layer</param>
     public void DuplicateLayer(Guid guidValue)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -110,6 +149,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
     }
 
+    /// <summary>
+    /// Delete the member with the <paramref name="guidValue"/>
+    /// </summary>
+    /// <param name="guidValue">The Guid of the layer</param>
     public void DeleteStructureMember(Guid guidValue)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -117,6 +160,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMember_Action(guidValue));
     }
 
+    /// <summary>
+    /// Deletes all members with the <paramref name="guids"/>
+    /// </summary>
+    /// <param name="guids">The Guids of the layers to delete</param>
     public void DeleteStructureMembers(IReadOnlyList<Guid> guids)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -124,6 +171,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(guids.Select(static guid => new DeleteStructureMember_Action(guid)).ToArray());
     }
 
+    /// <summary>
+    /// Resizes the canvas (Does not upscale the content of the image)
+    /// </summary>
+    /// <param name="newSize">The size the canvas should be resized to</param>
+    /// <param name="anchor">Where the existing content should be put</param>
     public void ResizeCanvas(VecI newSize, ResizeAnchor anchor)
     {
         if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
@@ -146,6 +198,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ResizeCanvas_Action(newSize, anchor));
     }
 
+    /// <summary>
+    /// Resizes the image (Upscales the content of the image)
+    /// </summary>
+    /// <param name="newSize">The size the image should be resized to</param>
+    /// <param name="resampling">The resampling method to use</param>
     public void ResizeImage(VecI newSize, ResamplingMethod resampling)
     {
         if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
@@ -168,6 +225,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ResizeImage_Action(newSize, resampling));
     }
 
+    /// <summary>
+    /// Replaces all <paramref name="oldColor"/> with <paramref name="newColor"/>
+    /// </summary>
+    /// <param name="oldColor">The color to replace</param>
+    /// <param name="newColor">The new color</param>
     public void ReplaceColor(Color oldColor, Color newColor)
     {
         if (Internals.ChangeController.IsChangeActive || oldColor == newColor)
@@ -175,6 +237,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor, newColor));
     }
 
+    /// <summary>
+    /// Creates a new mask on the <paramref name="member"/>
+    /// </summary>
     public void CreateMask(StructureMemberViewModel member)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -184,6 +249,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new CreateStructureMemberMask_Action(member.GuidValue));
     }
 
+    /// <summary>
+    /// Deletes the mask of the <paramref name="member"/>
+    /// </summary>
     public void DeleteMask(StructureMemberViewModel member)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -191,6 +259,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMemberMask_Action(member.GuidValue));
     }
     
+    /// <summary>
+    /// Applies the mask to the image
+    /// </summary>
     public void ApplyMask(StructureMemberViewModel member)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -199,14 +270,32 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ApplyMask_Action(member.GuidValue), new DeleteStructureMemberMask_Action(member.GuidValue));
     }
 
+    /// <summary>
+    /// Sets the selected structure memeber
+    /// </summary>
+    /// <param name="memberGuid">The Guid of the member to select</param>
     public void SetSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new SetSelectedMember_PassthroughAction(memberGuid));
 
+    /// <summary>
+    /// Adds a member to the soft selection
+    /// </summary>
+    /// <param name="memberGuid">The Guid of the member to add</param>
     public void AddSoftSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new AddSoftSelectedMember_PassthroughAction(memberGuid));
 
+    /// <summary>
+    /// Removes a member from the soft selection
+    /// </summary>
+    /// <param name="memberGuid">The Guid of the member to remove</param>
     public void RemoveSoftSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new RemoveSoftSelectedMember_PassthroughAction(memberGuid));
 
+    /// <summary>
+    /// Clears the soft selection
+    /// </summary>
     public void ClearSoftSelectedMembers() => Internals.ActionAccumulator.AddActions(new ClearSoftSelectedMembers_PassthroughAction());
 
+    /// <summary>
+    /// Undo last change
+    /// </summary>
     public void Undo()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -214,6 +303,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddActions(new Undo_Action());
     }
 
+    /// <summary>
+    /// Redo previously undone change
+    /// </summary>
     public void Redo()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -221,6 +313,12 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddActions(new Redo_Action());
     }
 
+    /// <summary>
+    /// Moves a member next to or inside another structure member
+    /// </summary>
+    /// <param name="memberToMove">The member to move</param>
+    /// <param name="memberToMoveIntoOrNextTo">The target member</param>
+    /// <param name="placement">Where to place the <paramref name="memberToMove"/></param>
     public void MoveStructureMember(Guid memberToMove, Guid memberToMoveIntoOrNextTo, StructureMemberPlacement placement)
     {
         if (Internals.ChangeController.IsChangeActive || memberToMove == memberToMoveIntoOrNextTo)
@@ -228,6 +326,9 @@ internal class DocumentOperationsModule
         Internals.StructureHelper.TryMoveStructureMember(memberToMove, memberToMoveIntoOrNextTo, placement);
     }
 
+    /// <summary>
+    /// Merge all structure members with the Guids inside <paramref name="members"/>
+    /// </summary>
     public void MergeStructureMembers(IReadOnlyList<Guid> members)
     {
         if (Internals.ChangeController.IsChangeActive || members.Count < 2)
@@ -248,6 +349,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddActions(new ChangeBoundary_Action());
     }
 
+    /// <summary>
+    /// Starts a image transform and pastes the transformed image on the currently selected layer
+    /// </summary>
+    /// <param name="image">The image to paste</param>
+    /// <param name="startPos">Where the transform should start</param>
     public void PasteImageWithTransform(Surface image, VecI startPos)
     {
         if (Document.SelectedStructureMember is null)
@@ -255,6 +361,20 @@ internal class DocumentOperationsModule
         Internals.ChangeController.TryStartExecutor(new PasteImageExecutor(image, startPos));
     }
 
+    /// <summary>
+    /// Starts a image transform and pastes the transformed image on the currently selected layer
+    /// </summary>
+    /// <param name="image">The image to paste</param>
+    /// <param name="startPos">Where the transform should start</param>
+    public void PasteImageWithTransform(Surface image, VecI startPos, Guid memberGuid, bool drawOnMask)
+    {
+        Internals.ChangeController.TryStartExecutor(new PasteImageExecutor(image, startPos, memberGuid, drawOnMask));
+    }
+
+    /// <summary>
+    /// Starts a transform on the selected area
+    /// </summary>
+    /// <param name="toolLinked">Is this transform started by a tool</param>
     public void TransformSelectedArea(bool toolLinked)
     {
         if (Document.SelectedStructureMember is null ||
@@ -264,6 +384,9 @@ internal class DocumentOperationsModule
         Internals.ChangeController.TryStartExecutor(new TransformSelectedAreaExecutor(toolLinked));
     }
 
+    /// <summary>
+    /// Ties stopping the currently executing tool linked executor
+    /// </summary>
     public void TryStopToolLinkedExecutor()
     {
         if (Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked)
@@ -273,6 +396,15 @@ internal class DocumentOperationsModule
     public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask) =>
         DrawImage(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, true);
 
+    /// <summary>
+    /// Draws a image on the member with the <paramref name="memberGuid"/>
+    /// </summary>
+    /// <param name="image">The image to draw onto the layer</param>
+    /// <param name="corners">The shape the image should fit into</param>
+    /// <param name="memberGuid">The Guid of the member to paste on</param>
+    /// <param name="ignoreClipSymmetriesEtc">Ignore selection clipping and symmetry (See DrawingChangeHelper.ApplyClipsSymmetriesEtc of UpdateableDocument)</param>
+    /// <param name="drawOnMask">Draw on the mask or on the image</param>
+    /// <param name="finish">Is this a finished action</param>
     private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, bool finish)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -284,6 +416,9 @@ internal class DocumentOperationsModule
             Internals.ActionAccumulator.AddFinishedActions();
     }
 
+    /// <summary>
+    /// Resizes the canvas to fit the content
+    /// </summary>
     public void ClipCanvas()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -291,8 +426,14 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action());
     }
 
+    /// <summary>
+    /// Flips the image on the <paramref name="flipType"/> axis
+    /// </summary>
     public void FlipImage(FlipType flipType) => FlipImage(flipType, null);
 
+    /// <summary>
+    /// Flips the members with the Guids of <paramref name="membersToFlip"/> on the <paramref name="flipType"/> axis
+    /// </summary>
     public void FlipImage(FlipType flipType, List<Guid> membersToFlip)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -301,8 +442,16 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, membersToFlip));
     }
 
+    /// <summary>
+    /// Rotates the image
+    /// </summary>
+    /// <param name="rotation">The degrees to rotate the image by</param>
     public void RotateImage(RotationAngle rotation) => RotateImage(rotation, null);
 
+    /// <summary>
+    /// Rotates the members with the Guids of <paramref name="membersToRotate"/>
+    /// </summary>
+    /// <param name="rotation">The degrees to rotate the members by</param>
     public void RotateImage(RotationAngle rotation, List<Guid> membersToRotate)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -311,6 +460,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate));
     }
     
+    /// <summary>
+    /// Puts the content of the image in the middle of the canvas
+    /// </summary>
     public void CenterContent(IReadOnlyList<Guid> structureMembers)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -319,6 +471,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new CenterContent_Action(structureMembers.ToList()));
     }
 
+    /// <summary>
+    /// Imports a reference layer from a Pbgra Int32 array
+    /// </summary>
+    /// <param name="imageSize">The size of the image</param>
     public void ImportReferenceLayer(ImmutableArray<byte> imagePbgra32Bytes, VecI imageSize)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -329,6 +485,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new SetReferenceLayer_Action(corners, imagePbgra32Bytes, imageSize));
     }
 
+    /// <summary>
+    /// Deletes the reference layer
+    /// </summary>
     public void DeleteReferenceLayer()
     {
         if (Internals.ChangeController.IsChangeActive || Document.ReferenceLayerViewModel.ReferenceBitmap is null)
@@ -337,6 +496,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DeleteReferenceLayer_Action());
     }
 
+    /// <summary>
+    /// Starts a transform on the reference layer
+    /// </summary>
     public void TransformReferenceLayer()
     {
         if (Document.ReferenceLayerViewModel.ReferenceBitmap is null || Internals.ChangeController.IsChangeActive)
@@ -344,6 +506,9 @@ internal class DocumentOperationsModule
         Internals.ChangeController.TryStartExecutor(new TransformReferenceLayerExecutor());
     }
 
+    /// <summary>
+    /// Resets the reference layer transform
+    /// </summary>
     public void ResetReferenceLayerPosition()
     {
         if (Document.ReferenceLayerViewModel.ReferenceBitmap is null || Internals.ChangeController.IsChangeActive)

+ 27 - 13
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -11,7 +11,7 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
     private readonly Surface image;
     private readonly VecI pos;
     private bool drawOnMask;
-    private Guid memberGuid;
+    private Guid? memberGuid;
 
     public PasteImageExecutor(Surface image, VecI pos)
     {
@@ -19,22 +19,36 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
         this.pos = pos;
     }
 
+    public PasteImageExecutor(Surface image, VecI pos, Guid memberGuid, bool drawOnMask)
+    {
+        this.image = image;
+        this.pos = pos;
+        this.memberGuid = memberGuid;
+        this.drawOnMask = drawOnMask;
+    }
+    
     public override ExecutionState Start()
     {
-        var member = document!.SelectedStructureMember;
-
-        if (member is null)
-            return ExecutionState.Error;
-        drawOnMask = member is LayerViewModel layer ? layer.ShouldDrawOnMask : true;
-        if (drawOnMask && !member.HasMaskBindable)
-            return ExecutionState.Error;
-        if (!drawOnMask && member is not LayerViewModel)
-            return ExecutionState.Error;
+        if (memberGuid == null)
+        {
+            var member = document!.SelectedStructureMember;
 
-        memberGuid = member.GuidValue;
+            if (member is null)
+                return ExecutionState.Error;
+            drawOnMask = member is not LayerViewModel layer || layer.ShouldDrawOnMask;
+            
+            switch (drawOnMask)
+            {
+                case true when !member.HasMaskBindable:
+                case false when member is not LayerViewModel:
+                    return ExecutionState.Error;
+            }
+            
+            memberGuid = member.GuidValue;
+        }
 
         ShapeCorners corners = new(new RectD(pos, image.Size));
-        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid, false, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask));
         document.TransformViewModel.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, corners, true);
 
         return ExecutionState.Success;
@@ -42,7 +56,7 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
 
     public override void OnTransformMoved(ShapeCorners corners)
     {
-        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid, false, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask));
     }
 
     public override void OnTransformApplied()

+ 12 - 0
src/PixiEditor/PixiEditor.csproj

@@ -356,6 +356,18 @@
 		<Resource Include="Images\Commands\PixiEditor\Layer\ToggleMask.png" />
 		<None Remove="Images\Commands\PixiEditor\Layer\ToggleVisible.png" />
 		<Resource Include="Images\Commands\PixiEditor\Layer\ToggleVisible.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate90Deg.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate90Deg.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate180Deg.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate180Deg.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate270Deg.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate270Deg.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate90DegLayers.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate90DegLayers.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate180DegLayers.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate180DegLayers.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate270DegLayers.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate270DegLayers.png" />
 	</ItemGroup>
 	<ItemGroup>
 		<None Include="..\LICENSE">

+ 11 - 42
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs

@@ -50,48 +50,20 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
     public bool DocumentNotNull() => ActiveDocument != null;
 
     [Command.Basic("PixiEditor.Document.ClipCanvas", "Clip Canvas", "Clip Canvas", CanExecute = "PixiEditor.HasDocument")]
-    public void ClipCanvas()
-    {
-        if (ActiveDocument is null)
-            return;
-        
-        ActiveDocument?.Operations.ClipCanvas();
-    }
-    
-    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", "Flip Image Horizontally", "Flip Image Horizontally", CanExecute = "PixiEditor.HasDocument")]
-    public void FlipImageHorizontally()
-    {
-        if (ActiveDocument is null)
-            return;
-        
-        ActiveDocument?.Operations.FlipImage(FlipType.Horizontal);
-    }
-    
-    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", "Flip Selected Layers Horizontally", "Flip Selected Layers Horizontally", CanExecute = "PixiEditor.HasDocument")]
-    public void FlipLayersHorizontally()
-    {
-        if (ActiveDocument?.SelectedStructureMember == null)
-            return;
-        
-        ActiveDocument?.Operations.FlipImage(FlipType.Horizontal, ActiveDocument.GetSelectedMembers());
-    }
-    
-    [Command.Basic("PixiEditor.Document.FlipImageVertical", "Flip Image Vertically", "Flip Image Vertically", CanExecute = "PixiEditor.HasDocument")]
-    public void FlipImageVertically()
-    {
-        if (ActiveDocument is null)
-            return;
-        
-        ActiveDocument?.Operations.FlipImage(FlipType.Vertical);
-    }
-    
-    [Command.Basic("PixiEditor.Document.FlipLayersVertical", "Flip Selected Layers Vertically", "Flip Selected Layers Vertically", CanExecute = "PixiEditor.HasDocument")]
-    public void FlipLayersVertically()
+    public void ClipCanvas() => ActiveDocument?.Operations.ClipCanvas();
+
+    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", FlipType.Horizontal, "Flip Image Horizontally", "Flip Image Horizontally", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Document.FlipImageVertical", FlipType.Vertical, "Flip Image Vertically", "Flip Image Vertically", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipImage(FlipType type) => ActiveDocument?.Operations.FlipImage(type);
+
+    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "Flip Selected Layers Horizontally", "Flip Selected Layers Horizontally", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Document.FlipLayersVertical", FlipType.Vertical, "Flip Selected Layers Vertically", "Flip Selected Layers Vertically", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipLayers(FlipType type)
     {
         if (ActiveDocument?.SelectedStructureMember == null)
             return;
         
-        ActiveDocument?.Operations.FlipImage(FlipType.Vertical, ActiveDocument.GetSelectedMembers());
+        ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers());
     }
     
     [Command.Basic("PixiEditor.Document.Rotate90Deg", "Rotate Image 90 degrees", 
@@ -100,10 +72,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         "Rotate Image 180 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
     [Command.Basic("PixiEditor.Document.Rotate270Deg", "Rotate Image -90 degrees", 
         "Rotate Image -90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
-    public void RotateImage(RotationAngle angle)
-    {
-        ActiveDocument?.Operations.RotateImage(angle);
-    }
+    public void RotateImage(RotationAngle angle) => ActiveDocument?.Operations.RotateImage(angle);
 
     [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "Rotate Selected Layers 90 degrees", 
         "Rotate Selected Layers 90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]

+ 2 - 2
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs

@@ -23,7 +23,7 @@ internal partial class DocumentViewModel
     {
         var root = new Folder();
         
-        IReadOnlyDocument doc = Internals.Tracker.Document;
+        var doc = Internals.Tracker.Document;
 
         AddMembers(doc.StructureRoot.Children, doc, root);
 
@@ -31,7 +31,7 @@ internal partial class DocumentViewModel
         {
             Width = Width, Height = Height,
             Swatches = ToCollection(Swatches), Palette = ToCollection(Palette),
-            RootFolder = root, PreviewImage = PreviewSurface.Snapshot().Encode().AsSpan().ToArray()
+            RootFolder = root, PreviewImage = (MaybeRenderWholeImage().Value as Surface)?.DrawingSurface.Snapshot().Encode().AsSpan().ToArray()
         };
 
         return document;

+ 16 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -177,6 +177,10 @@ internal partial class DocumentViewModel : NotifyableObject
         ReferenceLayerViewModel = new(this, Internals);
     }
 
+    /// <summary>
+    /// Creates a new document using the <paramref name="builder"/>
+    /// </summary>
+    /// <returns>The created document</returns>
     public static DocumentViewModel Build(Action<DocumentViewModelBuilder> builder)
     {
         var builderInstance = new DocumentViewModelBuilder();
@@ -282,6 +286,10 @@ internal partial class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(AllChangesSaved));
     }
 
+    /// <summary>
+    /// Tries rendering the whole document
+    /// </summary>
+    /// <returns><see cref="Error"/> if the ChunkyImage was disposed, otherwise a <see cref="Surface"/> of the rendered document</returns>
     public OneOf<Error, Surface> MaybeRenderWholeImage()
     {
         try
@@ -360,6 +368,11 @@ internal partial class DocumentViewModel : NotifyableObject
         return (output, bounds);
     }
 
+    /// <summary>
+    /// Picks the color at <paramref name="pos"/>
+    /// </summary>
+    /// <param name="includeReference">Should the color be picked from the reference layer</param>
+    /// <param name="includeCanvas">Should the color be picked from the canvas</param>
     public Color PickColor(VecD pos, DocumentScope scope, bool includeReference, bool includeCanvas)
     {
         if (scope == DocumentScope.SingleLayer && includeReference && includeCanvas)
@@ -489,6 +502,9 @@ internal partial class DocumentViewModel : NotifyableObject
     public void InternalRemoveSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Remove(member);
     #endregion
 
+    /// <summary>
+    /// Returns a list of all selected members (Hard and Soft selected)
+    /// </summary>
     public List<Guid> GetSelectedMembers()
     {
         List<Guid> layerGuids = new List<Guid>() { SelectedStructureMember.GuidValue };

+ 73 - 35
src/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs

@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Media;
+using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Controllers;
 
@@ -26,21 +27,32 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         doc.Operations.DeleteSelectedPixels(true);
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Paste", "Paste", "Paste from clipboard", CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = ModifierKeys.Control)]
-    public void Paste()
+    [Command.Basic("PixiEditor.Clipboard.Paste", false, "Paste", "Paste from clipboard", CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = ModifierKeys.Shift)]
+    [Command.Basic("PixiEditor.Clipboard.PasteAsNewLayer", true, "Paste as new layer", "Paste from clipboard as new layer", CanExecute = "PixiEditor.Clipboard.CanPaste", IconPath = "$PixiEditor.Clipboard.Paste", Key = Key.V, Modifiers = ModifierKeys.Control)]
+    public void Paste(bool pasteAsNewLayer)
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is null) 
             return;
-        ClipboardController.TryPasteFromClipboard(Owner.DocumentManagerSubViewModel.ActiveDocument);
+        ClipboardController.TryPasteFromClipboard(Owner.DocumentManagerSubViewModel.ActiveDocument, pasteAsNewLayer);
     }
-
-    [Command.Basic("PixiEditor.Clipboard.PasteColor", "Paste color", "Paste color from clipboard", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
-    public void PasteColor()
+    
+    [Command.Basic("PixiEditor.Clipboard.PasteColor", false, "Paste color", "Paste color from clipboard", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.PasteColorAsSecondary", true, "Paste color as secondary", "Paste color as secondary from clipboard", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
+    public void PasteColor(bool secondary)
     {
-        if (ParseAnyFormat(Clipboard.GetText().Trim(), out var result))
+        if (!ColorHelper.ParseAnyFormat(Clipboard.GetText().Trim(), out var result))
+        {
+            return;
+        }
+
+        if (!secondary)
         {
             Owner.ColorsSubViewModel.PrimaryColor = result.Value;
         }
+        else
+        {
+            Owner.ColorsSubViewModel.SecondaryColor = result.Value;
+        }
     }
 
     [Command.Basic("PixiEditor.Clipboard.Copy", "Copy", "Copy to clipboard", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.C, Modifiers = ModifierKeys.Control)]
@@ -52,6 +64,31 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         ClipboardController.CopyToClipboard(doc);
     }
 
+    [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsHex", CopyColor.PrimaryHEX, "Copy primary color (HEX)", "Copy primary color as hex code", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsRgb", CopyColor.PrimaryRGB, "Copy primary color (RGB)", "Copy primary color as RGB code", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsHex", CopyColor.SecondaryHEX, "Copy secondary color (HEX)", "Copy secondary color as hex code", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsRgb", CopyColor.SecondardRGB, "Copy secondary color (RGB)", "Copy secondary color as RGB code", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
+    public void CopyColorAsHex(CopyColor color)
+    {
+        var targetColor = color switch
+        {
+            CopyColor.PrimaryHEX or CopyColor.PrimaryRGB => Owner.ColorsSubViewModel.PrimaryColor,
+            _ => Owner.ColorsSubViewModel.SecondaryColor
+        };
+
+        string text = color switch
+        {
+            CopyColor.PrimaryHEX or CopyColor.SecondaryHEX => targetColor.A == 255
+                ? $"#{targetColor.R:X2}{targetColor.G:X2}{targetColor.B:X2}"
+                : targetColor.ToString(),
+            _ => targetColor.A == 255
+                ? $"rgb({targetColor.R},{targetColor.G},{targetColor.B})"
+                : $"rgba({targetColor.R},{targetColor.G},{targetColor.B},{targetColor.A})",
+        };
+
+        Clipboard.SetText(text);
+    }
+
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPaste")]
     public bool CanPaste()
     {
@@ -59,50 +96,51 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteColor")]
-    public static bool CanPasteColor() => ParseAnyFormat(Clipboard.GetText().Trim(), out _);
+    public static bool CanPasteColor() => ColorHelper.ParseAnyFormat(Clipboard.GetText().Trim(), out _);
 
     [Evaluator.Icon("PixiEditor.Clipboard.PasteColorIcon")]
     public static ImageSource GetPasteColorIcon()
     {
         Color color;
 
-        if (ParseAnyFormat(Clipboard.GetText().Trim(), out var result))
-        {
-            color = result.Value.ToOpaqueMediaColor();
-        }
-        else
-        {
-            color = Colors.Transparent;
-        }
+        color = ColorHelper.ParseAnyFormat(Clipboard.GetText().Trim(), out var result) ? result.Value.ToOpaqueMediaColor() : Colors.Transparent;
 
         return ColorSearchResult.GetIcon(color.ToOpaqueColor());
     }
 
-    private static bool ParseAnyFormat(string value, [NotNullWhen(true)] out DrawingApi.Core.ColorsImpl.Color? result)
+    [Evaluator.Icon("PixiEditor.Clipboard.CopyColorIcon")]
+    public ImageSource GetCopyColorIcon(object data)
     {
-        bool hex = Regex.IsMatch(Clipboard.GetText().Trim(), "^#?([a-fA-F0-9]{8}|[a-fA-F0-9]{6}|[a-fA-F0-9]{3})$");
-
-        if (hex)
+        if (data is CopyColor color)
         {
-            result = DrawingApi.Core.ColorsImpl.Color.Parse(Clipboard.GetText().Trim());
-            return true;
         }
-
-        var match = Regex.Match(Clipboard.GetText().Trim(), @"(?:rgba?\(?)? *(?<r>\d{1,3})(?:, *| +)(?<g>\d{1,3})(?:, *| +)(?<b>\d{1,3})(?:(?:, *| +)(?<a>\d{0,3}))?\)?");
-
-        if (!match.Success)
+        else if (data is Models.Commands.Commands.Command.BasicCommand command)
         {
-            result = null;
-            return false;
+            color = (CopyColor)command.Parameter;
         }
+        else if (data is CommandSearchResult result)
+        {
+            color = (CopyColor)((Models.Commands.Commands.Command.BasicCommand)result.Command).Parameter;
+        }
+        else
+        {
+            throw new ArgumentException("data must be of type CopyColor, BasicCommand or CommandSearchResult");
+        }
+        
+        var targetColor = color switch
+        {
+            CopyColor.PrimaryHEX or CopyColor.PrimaryRGB => Owner.ColorsSubViewModel.PrimaryColor,
+            _ => Owner.ColorsSubViewModel.SecondaryColor
+        };
 
-        byte r = byte.Parse(match.Groups["r"].ValueSpan);
-        byte g = byte.Parse(match.Groups["g"].ValueSpan);
-        byte b = byte.Parse(match.Groups["b"].ValueSpan);
-        byte a = match.Groups["a"].Success ? byte.Parse(match.Groups["a"].ValueSpan) : (byte)255;
-
-        result = new DrawingApi.Core.ColorsImpl.Color(r, g, b, a);
-        return true;
+        return ColorSearchResult.GetIcon(targetColor.ToOpaqueMediaColor().ToOpaqueColor());
+    }
 
+    public enum CopyColor
+    {
+        PrimaryHEX,
+        PrimaryRGB,
+        SecondaryHEX,
+        SecondardRGB
     }
 }

+ 17 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -1,6 +1,7 @@
 using System.Diagnostics;
 using System.IO;
 using System.Reflection;
+using System.Windows.Input;
 using Microsoft.Win32;
 using Newtonsoft.Json;
 using PixiEditor.Helpers;
@@ -9,6 +10,7 @@ using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Templates.Parsers;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.UserPreferences;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
@@ -130,6 +132,21 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         }
     }
 
+    [Command.Debug("PixiEditor.Debug.ClearRecentDocument", "Clear recent documents", "Clear recently opened documents")]
+    public void ClearRecentDocuments()
+    {
+        Owner.FileSubViewModel.RecentlyOpened.Clear();
+        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, Array.Empty<object>());
+    }
+
+    [Command.Debug("PixiEditor.Debug.OpenCommandDebugWindow", "Open command debug window", "Open command debug window")]
+    public void OpenCommandDebugWindow()
+    {
+        Mouse.OverrideCursor = Cursors.Wait;
+        new CommandDebugPopup().Show();
+        Mouse.OverrideCursor = null;
+    }
+
     [Command.Debug("PixiEditor.Debug.OpenInstallDirectory", "Open Installation Directory", "Open Installation Directory", IconPath = "Folder.png")]
     public static void OpenInstallLocation()
     {

+ 11 - 5
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -57,14 +57,21 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     public void AddRecentlyOpened(string path)
     {
         if (RecentlyOpened.Contains(path))
-            return;
-        
-        RecentlyOpened.Insert(0, path);
-        int maxCount = IPreferences.Current.GetPreference<int>(PreferencesConstants.MaxOpenedRecently, PreferencesConstants.MaxOpenedRecentlyDefault);
+        {
+            RecentlyOpened.Move(RecentlyOpened.IndexOf(path), 0);
+        }
+        else
+        {
+            RecentlyOpened.Insert(0, path);
+        }
+
+        int maxCount = IPreferences.Current.GetPreference(PreferencesConstants.MaxOpenedRecently, PreferencesConstants.MaxOpenedRecentlyDefault);
+
         while (RecentlyOpened.Count > maxCount)
         {
             RecentlyOpened.RemoveAt(RecentlyOpened.Count - 1);
         }
+
         IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, RecentlyOpened.Select(x => x.FilePath));
     }
 
@@ -299,7 +306,6 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return;
-        ViewModelMain.Current.ActionDisplay = "";
 
         ExportFileDialog info = new ExportFileDialog(doc.SizeBindable);
         if (info.ShowDialog())

+ 54 - 5
src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -1,12 +1,17 @@
 using System.Collections.Immutable;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Media.Imaging;
+using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using Microsoft.Win32;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.IO;
@@ -15,7 +20,7 @@ using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 #nullable enable
-[Command.Group("PixiEditor.Layer", "Image")]
+[Command.Group("PixiEditor.Layer", "Layer")]
 internal class LayersViewModel : SubViewModel<ViewModelMain>
 {
     public LayersViewModel(ViewModelMain owner)
@@ -134,10 +139,8 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Internal("PixiEditor.Layer.OpacitySliderDragged")]
-    public void OpacitySliderDragged(object parameter)
+    public void OpacitySliderDragged(double value)
     {
-        if (parameter is not double value)
-            return;
         Owner.DocumentManagerSubViewModel.ActiveDocument?.EventInlet.OnOpacitySliderDragged((float)value);
     }
 
@@ -147,6 +150,17 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         Owner.DocumentManagerSubViewModel.ActiveDocument?.EventInlet.OnOpacitySliderDragEnded();
     }
 
+    [Command.Internal("PixiEditor.Layer.OpacitySliderSet")]
+    public void OpacitySliderSet(double value)
+    {
+        var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
+
+        if (document?.SelectedStructureMember != null)
+        {
+            document.Operations.SetMemberOpacity(document.SelectedStructureMember.GuidValue, (float)value);
+        }
+    }
+
     [Command.Basic("PixiEditor.Layer.DuplicateSelectedLayer", "Duplicate selected layer", "Duplicate selected layer", CanExecute = "PixiEditor.Layer.SelectedMemberIsLayer")]
     public void DuplicateLayer()
     {
@@ -314,7 +328,23 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     public bool ReferenceLayerExists() => Owner.DocumentManagerSubViewModel.ActiveDocument?.ReferenceLayerViewModel.ReferenceBitmap is not null;
     [Evaluator.CanExecute("PixiEditor.Layer.ReferenceLayerDoesntExist")]
     public bool ReferenceLayerDoesntExist() => 
-        Owner.DocumentManagerSubViewModel.ActiveDocument is null ? false : Owner.DocumentManagerSubViewModel.ActiveDocument.ReferenceLayerViewModel.ReferenceBitmap is null;
+        Owner.DocumentManagerSubViewModel.ActiveDocument is not null && Owner.DocumentManagerSubViewModel.ActiveDocument.ReferenceLayerViewModel.ReferenceBitmap is null;
+
+    [Evaluator.CanExecute("PixiEditor.Layer.ReferenceLayerDoesntExistAndHasClipboardContent")]
+    public bool ReferenceLayerDoesntExistAndHasClipboardContent(DataObject data)
+    {
+        if (!ReferenceLayerDoesntExist())
+        {
+            return false;
+        }
+        
+        if (data != null)
+        {
+            return Owner.DocumentIsNotNull(null) && ClipboardController.IsImage(data);
+        }
+        
+        return Owner.ClipboardSubViewModel.CanPaste();
+    }
 
     [Command.Basic("PixiEditor.Layer.ImportReferenceLayer", "Add reference layer", "Add reference layer", CanExecute = "PixiEditor.Layer.ReferenceLayerDoesntExist")]
     public void ImportReferenceLayer()
@@ -347,6 +377,25 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
             pixels.ToImmutableArray(), 
             size);
     }
+
+    [Command.Basic("PixiEditor.Layer.PasteReferenceLayer", "Paste reference layer", "Paste reference layer from clipboard", IconPath = "Commands/PixiEditor/Clipboard/Paste.png", CanExecute = "PixiEditor.Layer.ReferenceLayerDoesntExistAndHasClipboardContent")]
+    public void PasteReferenceLayer(DataObject data)
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+
+        var surface = (data == null ? ClipboardController.GetImagesFromClipboard() : ClipboardController.GetImage(data)).First();
+        using var image = surface.image;
+        
+        var bitmap = surface.image.ToWriteableBitmap();
+
+        byte[] pixels = new byte[bitmap.PixelWidth * bitmap.PixelHeight * 4];
+        bitmap.CopyPixels(pixels, bitmap.PixelWidth * 4, 0);
+
+        doc.Operations.ImportReferenceLayer(
+            pixels.ToImmutableArray(),
+            surface.image.Size);
+    }
+    
     private string OpenReferenceLayerFilePicker()
     {
         var imagesFilter = new FileTypeDialogDataSet(FileTypeDialogDataSet.SetKind.Images).GetFormattedTypes();

+ 5 - 32
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -2,6 +2,7 @@
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.Helpers;
+using PixiEditor.Helpers.Collections;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
@@ -16,9 +17,6 @@ namespace PixiEditor.ViewModels;
 
 internal class ViewModelMain : ViewModelBase
 {
-    private string actionDisplay;
-    private bool overrideActionDisplay;
-
     public static ViewModelMain Current { get; set; }
 
     public IServiceProvider Services { get; private set; }
@@ -73,39 +71,14 @@ internal class ViewModelMain : ViewModelBase
 
     public IPreferences Preferences { get; set; }
 
-    public string ActionDisplay
-    {
-        get
-        {
-            if (OverrideActionDisplay)
-            {
-                return actionDisplay;
-            }
-
-            return ToolsSubViewModel.ActiveTool?.ActionDisplay;
-        }
-        set
-        {
-            actionDisplay = value;
-        }
-    }
+    public string ActiveActionDisplay => ActionDisplays.HasActive() ? ActionDisplays.GetActive() : ToolsSubViewModel.ActiveTool?.ActionDisplay;
 
-    /// <summary>
-    /// Gets or sets a value indicating whether a custom action display should be used. If false the action display of the selected tool will be used.
-    /// </summary>
-    public bool OverrideActionDisplay
-    {
-        get => overrideActionDisplay;
-        set
-        {
-            SetProperty(ref overrideActionDisplay, value);
-            RaisePropertyChanged(nameof(ActionDisplay));
-        }
-    }
+    public ActionDisplayList ActionDisplays { get; }
 
     public ViewModelMain(IServiceProvider serviceProvider)
     {
         Current = this;
+        ActionDisplays = new ActionDisplayList(() => RaisePropertyChanged(nameof(ActiveActionDisplay)));
     }
 
     public void Setup(IServiceProvider services)
@@ -195,7 +168,7 @@ internal class ViewModelMain : ViewModelBase
 
     private void NotifyToolActionDisplayChanged()
     {
-        if (!OverrideActionDisplay) RaisePropertyChanged(nameof(ActionDisplay));
+        if (!ActionDisplays.Any()) RaisePropertyChanged(nameof(ActiveActionDisplay));
     }
 
     /// <summary>

+ 83 - 0
src/PixiEditor/Views/Dialogs/CommandDebugPopup.xaml

@@ -0,0 +1,83 @@
+<Window x:Class="PixiEditor.Views.Dialogs.CommandDebugPopup"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:local="clr-namespace:PixiEditor.Views.Dialogs"
+        xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
+        xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
+        xmlns:command="clr-namespace:PixiEditor.Models.Commands"
+        xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML" xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+        WindowStyle="None"
+        mc:Ignorable="d"
+        x:Name="uc"
+        Foreground="White"
+        Title="Command Debug" Height="450" Width="800">
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32" GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <DockPanel Background="{StaticResource AccentColor}" Focusable="True">
+        <b:Interaction.Behaviors>
+            <behaviours:ClearFocusOnClickBehavior />
+        </b:Interaction.Behaviors>
+
+        <local:DialogTitleBar DockPanel.Dock="Top"
+                              CloseCommand="{x:Static SystemCommands.CloseWindowCommand}"
+                              TitleText="Command Debug" />
+
+        <Grid>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition />
+            </Grid.RowDefinitions>
+
+            <StackPanel Orientation="Horizontal" Margin="5">
+                <Button Content="Export list" Style="{StaticResource DarkRoundButton}" Command="{cmds:Command PixiEditor.Debug.DumpAllCommands}" Width="100"/>
+            </StackPanel>
+
+            <ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="1">
+                <ItemsControl ItemsSource="{Binding Commands, ElementName=uc}" Margin="5,0,0,5">
+                    <ItemsControl.ItemTemplate>
+                        <DataTemplate>
+                            <Border BorderThickness="0,0,0,1" BorderBrush="{StaticResource BrighterAccentColor}">
+                                <Grid Margin="0,5,0,5">
+                                    <Grid.ColumnDefinitions>
+                                        <ColumnDefinition Width="35"/>
+                                        <ColumnDefinition/>
+                                        <ColumnDefinition/>
+                                        <ColumnDefinition/>
+                                    </Grid.ColumnDefinitions>
+                                    <Grid.RowDefinitions>
+                                        <RowDefinition/>
+                                        <RowDefinition/>
+                                        <RowDefinition/>
+                                    </Grid.RowDefinitions>
+
+                                    <Image Grid.RowSpan="3" Source="{Binding Image}" Margin="0,0,5,0"/>
+
+                                    <TextBlock Text="{Binding Command.InternalName}" Grid.Column="1"/>
+                                    <TextBlock Text="{Binding Command.DisplayName, Converter={converters:EmptyStringFillerConverter EmptyText='[internal]', NullText='[internal]'}}" Grid.Column="2" />
+                                    <TextBlock Text="{Binding Command.Description, Converter={converters:EmptyStringFillerConverter}}" Grid.Column="3" />
+
+                                    <usercontrols:PrependTextBlock Prepend="Default Shortcut: '" Text="{Binding Command.DefaultShortcut}" Append="'"  Grid.Row="1" Grid.Column="1"/>
+                                    <usercontrols:PrependTextBlock Prepend="Current Shortcut: '" Text="{Binding Command.Shortcut}" Append="'" Grid.Row="1" Grid.Column="2"/>
+                                    <usercontrols:PrependTextBlock Prepend="Is Debug: '" Text="{Binding Command.IsDebug}" Append="'" Grid.Row="1" Grid.Column="3"/>
+
+                                    <ContentControl Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Content="{Binding Comments}"/>
+                                </Grid>
+                            </Border>
+                        </DataTemplate>
+                    </ItemsControl.ItemTemplate>
+                </ItemsControl>
+            </ScrollViewer>
+        </Grid>
+    </DockPanel>
+</Window>

+ 142 - 0
src/PixiEditor/Views/Dialogs/CommandDebugPopup.xaml.cs

@@ -0,0 +1,142 @@
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.Commands;
+using PixiEditor.Models.Commands.Evaluators;
+using PixiEditor.Models.DataHolders;
+
+namespace PixiEditor.Views.Dialogs;
+
+public partial class CommandDebugPopup : Window
+{
+    private static Brush infoBrush = new SolidColorBrush(Color.FromRgb(129, 143, 156));
+
+    private static Brush warningBrush = new SolidColorBrush(Color.FromRgb(222, 130, 55));
+
+    private static Brush errorBrush = new SolidColorBrush(Color.FromRgb(230, 34, 57));
+
+    public static readonly DependencyProperty CommandsProperty =
+        DependencyProperty.Register(nameof(Commands), typeof(IEnumerable<CommandDebug>), typeof(CommandDebugPopup));
+
+    internal IEnumerable<CommandDebug> Commands
+    {
+        get => (IEnumerable<CommandDebug>)GetValue(CommandsProperty);
+        set => SetValue(CommandsProperty, value);
+    }
+
+    public CommandDebugPopup()
+    {
+        var debugCommands = new List<CommandDebug>();
+
+        foreach (var command in CommandController.Current.Commands)
+        {
+            var comments = new TextBlock { TextWrapping = TextWrapping.Wrap };
+
+            ImageSource image = null;
+            Exception imageException = null;
+
+            try
+            {
+                image = command.GetIcon();
+            }
+            catch (Exception e)
+            {
+                imageException = e;
+            }
+
+            var analysis = AnalyzeCommand(command, image, imageException, out int issues);
+
+            foreach (var inline in analysis)
+            {
+                comments.Inlines.Add(inline);
+            }
+
+            debugCommands.Add(new CommandDebug(command, comments, image, issues));
+        }
+
+        Commands = debugCommands.OrderByDescending(x => x.Issues).ThenBy(x => x.Command.InternalName).ToArray();
+
+        InitializeComponent();
+    }
+
+    private List<Inline> AnalyzeCommand(Command command, ImageSource? image, Exception? imageException, out int issues)
+    {
+        var inlines = new List<Inline>();
+        issues = 0;
+
+        if (imageException != null)
+        {
+            Error($"Icon evaluator throws exception\n{imageException}\n");
+            issues++;
+        }
+        else
+        {
+            if (image == null && command.IconEvaluator == IconEvaluator.Default)
+            {
+                var expected = IconEvaluator.GetDefaultPath(command);
+
+                if (string.IsNullOrWhiteSpace(command.IconPath))
+                {
+                    Info(
+                        $"Default evaluator has not found a image (No icon path provided). Expected at '{expected}'\n");
+                }
+                else
+                {
+                    Error($"Default evaluator has not found a image at icon path! Expected at '{expected}'.\n");
+                    issues++;
+                }
+            }
+        }
+
+        if (command.IconEvaluator != IconEvaluator.Default)
+        {
+            Info($"Uses custom icon evaluator ({command.IconEvaluator.GetType().Name})\n");
+        }
+
+        if (!string.IsNullOrWhiteSpace(command.IconPath))
+        {
+            Info($"Has custom icon path: '{command.IconPath}'\n");
+        }
+
+        return inlines;
+
+        void Info(string text) => inlines.Add(new Run(text) { Foreground = infoBrush });
+
+        void Warning(string text) => inlines.Add(new Run(text) { Foreground = warningBrush });
+
+        void Error(string text) => inlines.Add(new Run(text) { Foreground = errorBrush });
+    }
+
+    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+    {
+        e.CanExecute = true;
+    }
+
+    private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+    {
+        SystemCommands.CloseWindow(this);
+    }
+
+    internal class CommandDebug
+    {
+        public Command Command { get; }
+
+        public TextBlock Comments { get; }
+
+        public ImageSource Image { get; }
+
+        public int Issues { get; }
+
+        public CommandDebug(Command command, TextBlock comments, ImageSource image, int issues)
+        {
+            Command = command;
+            Comments = comments;
+            Image = image;
+            Issues = issues;
+        }
+    }
+}

+ 11 - 3
src/PixiEditor/Views/MainWindow.xaml

@@ -323,6 +323,10 @@
                     <MenuItem
                         Header="_Debug"
                         Visibility="{Binding DebugSubViewModel.UseDebug, Converter={StaticResource BoolToVisibilityConverter}}">
+                        <MenuItem
+                            Header="Open Command Debug Window"
+                            cmds:Menu.Command="PixiEditor.Debug.OpenCommandDebugWindow"/>
+                        <Separator/>
                         <MenuItem
                             Header="Open _Local App Data"
                             cmds:Menu.Command="PixiEditor.Debug.OpenLocalAppDataDirectory" />
@@ -336,7 +340,7 @@
                             Header="Open _Install Location"
                             cmds:Menu.Command="PixiEditor.Debug.OpenInstallDirectory" />
                         <MenuItem
-                            Header="Open Crash Reports Location"
+                            Header="Open Crash _Reports Location"
                             cmds:Menu.Command="PixiEditor.Debug.OpenCrashReportsDirectory" />
                         <Separator />
                         <MenuItem
@@ -353,6 +357,10 @@
                             <MenuItem
                                 Header="Editor Data (Local)"
                                 cmds:Menu.Command="PixiEditor.Debug.DeleteEditorData" />
+                            <Separator/>
+                            <MenuItem
+                                Header="_Clear recent documents"
+                                cmds:Menu.Command="PixiEditor.Debug.ClearRecentDocument"/>
                         </MenuItem>
                     </MenuItem>
                 </cmds:Menu>
@@ -496,7 +504,7 @@
             </StackPanel>
 
             <Grid Grid.Column="1" Grid.Row="2" Background="#303030" >
-                <Grid AllowDrop="True" Drop="MainWindow_Drop">
+                <Grid AllowDrop="True" Drop="MainWindow_Drop" DragEnter="MainWindow_DragEnter" DragLeave="MainWindow_DragLeave">
                     <DockingManager 
                         ActiveContent="{Binding WindowSubViewModel.ActiveWindow, Mode=TwoWay}"
                         DocumentsSource="{Binding WindowSubViewModel.Viewports}">
@@ -785,7 +793,7 @@
                 </Grid.ColumnDefinitions>
                 <DockPanel>
                     <TextBlock
-                        Text="{Binding ActionDisplay}"
+                        Text="{Binding ActiveActionDisplay}"
                         Foreground="White"
                         FontSize="15"
                         Margin="10,0,0,0"

+ 40 - 7
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -6,6 +6,8 @@ using System.Windows.Media.Imaging;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Skia;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -184,16 +186,47 @@ internal partial class MainWindow : Window
 
     private void MainWindow_Drop(object sender, DragEventArgs e)
     {
-        if (e.Data.GetDataPresent(DataFormats.FileDrop))
+        DataContext.ActionDisplays[nameof(MainWindow_Drop)] = null;
+        
+        if (!e.Data.GetDataPresent(DataFormats.FileDrop))
         {
-            string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
-            if (files != null && files.Length > 0)
+            if (!ColorHelper.ParseAnyFormat(e.Data, out var color))
             {
-                if (Importer.IsSupportedFile(files[0]))
-                {
-                    DataContext.FileSubViewModel.OpenFromPath(files[0]);
-                }
+                return;
             }
+
+            DataContext.ColorsSubViewModel.PrimaryColor = color.Value;
+            return;
+        }
+
+        string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
+        
+        if (files is { Length: > 0 } && Importer.IsSupportedFile(files[0]))
+        {
+            DataContext.FileSubViewModel.OpenFromPath(files[0]);
         }
     }
+
+    private void MainWindow_DragEnter(object sender, DragEventArgs e)
+    {
+        if (!ClipboardController.IsImage((DataObject)e.Data))
+        {
+            if (ColorHelper.ParseAnyFormat(e.Data, out _))
+            {
+                DataContext.ActionDisplays[nameof(MainWindow_Drop)] = "Paste as primary color";
+                return;
+            }
+            
+            e.Effects = DragDropEffects.None;
+            e.Handled = true;
+            return;
+        }
+
+        DataContext.ActionDisplays[nameof(MainWindow_Drop)] = "Import as new file";
+    }
+
+    private void MainWindow_DragLeave(object sender, DragEventArgs e)
+    {
+        DataContext.ActionDisplays[nameof(MainWindow_Drop)] = null;
+    }
 }

+ 1 - 0
src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml

@@ -128,6 +128,7 @@
                                 DragStarted="{commands:Command PixiEditor.Layer.OpacitySliderDragStarted}"
                                 DragValueChanged="{commands:Command PixiEditor.Layer.OpacitySliderDragged, UseProvided=True}"
                                 DragEnded="{commands:Command PixiEditor.Layer.OpacitySliderDragEnded}"
+                                SetOpacity="{commands:Command PixiEditor.Layer.OpacitySliderSet, UseProvided=True}"
                                 ValueFromSlider="{Binding ElementName=opacitySlider, Path=Value, Mode=TwoWay}" />
                     </i:Interaction.Behaviors>
                 </Slider>

+ 44 - 3
src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml.cs

@@ -3,6 +3,7 @@ using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Threading;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -114,20 +115,60 @@ internal partial class LayersManager : UserControl
 
     private void Grid_Drop(object sender, DragEventArgs e)
     {
+        ViewModelMain.Current.ActionDisplays[nameof(LayersManager)] = null;
+        
+        if (ActiveDocument == null)
+        {
+            return;
+        }
+
         dropBorder.BorderBrush = Brushes.Transparent;
         Guid? droppedGuid = LayerControl.ExtractMemberGuid(e.Data);
-        if (droppedGuid is null || ActiveDocument is null)
-            return;
-        ActiveDocument.Operations.MoveStructureMember((Guid)droppedGuid, ActiveDocument.StructureRoot.Children[0].GuidValue, StructureMemberPlacement.Below);
+
+        if (droppedGuid is not null && ActiveDocument is not null)
+        {
+            ActiveDocument.Operations.MoveStructureMember((Guid)droppedGuid,
+                ActiveDocument.StructureRoot.Children[0].GuidValue, StructureMemberPlacement.Below);
+            e.Handled = true;
+        }
+
+        if (ClipboardController.TryPaste(ActiveDocument, (DataObject)e.Data, true))
+        {
+            e.Handled = true;
+        }
     }
 
     private void Grid_DragEnter(object sender, DragEventArgs e)
     {
+        if (ActiveDocument == null)
+        {
+            return;
+        }
+        
+        var member = LayerControl.ExtractMemberGuid(e.Data);
+
+        if (member == null)
+        {
+            if (!ClipboardController.IsImage((DataObject)e.Data))
+            {
+                return;
+            }
+
+            ViewModelMain.Current.ActionDisplays[nameof(LayersManager)] = "Import as new layer";
+            e.Effects = DragDropEffects.Copy;
+        }
+        else
+        {
+            e.Effects = DragDropEffects.Move;
+        }
+        
         ((Border)sender).BorderBrush = highlightColor;
+        e.Handled = true;
     }
 
     private void Grid_DragLeave(object sender, DragEventArgs e)
     {
+        ViewModelMain.Current.ActionDisplays[nameof(LayersManager)] = null;
         ((Border)sender).BorderBrush = Brushes.Transparent;
     }
 

+ 2 - 1
src/PixiEditor/Views/UserControls/Layers/ReferenceLayer.xaml

@@ -12,7 +12,8 @@
              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
              mc:Ignorable="d" 
              d:DesignHeight="60" d:DesignWidth="350" VerticalAlignment="Center" Name="uc">
-    <Border BorderBrush="{StaticResource DarkerAccentColor}" BorderThickness="0 2 0 0" MinWidth="60" Focusable="True">
+    <Border BorderBrush="{StaticResource DarkerAccentColor}" BorderThickness="0 2 0 0" MinWidth="60"
+            Focusable="True" AllowDrop="True" DragEnter="ReferenceLayer_DragEnter" DragLeave="ReferenceLayer_DragLeave" Drop="ReferenceLayer_Drop">
         <i:Interaction.Behaviors>
             <behaviors:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>

+ 33 - 0
src/PixiEditor/Views/UserControls/Layers/ReferenceLayer.xaml.cs

@@ -2,6 +2,9 @@
 using System.Windows.Controls;
 using System.Windows.Input;
 using Microsoft.Win32;
+using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.Commands;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.IO;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
@@ -10,6 +13,8 @@ namespace PixiEditor.Views.UserControls.Layers;
 #nullable enable
 internal partial class ReferenceLayer : UserControl
 {
+    private Command command;
+    
     public static readonly DependencyProperty DocumentProperty =
         DependencyProperty.Register(nameof(Document), typeof(DocumentViewModel), typeof(ReferenceLayer), new(null));
 
@@ -21,6 +26,34 @@ internal partial class ReferenceLayer : UserControl
 
     public ReferenceLayer()
     {
+        command = CommandController.Current.Commands["PixiEditor.Layer.PasteReferenceLayer"];
         InitializeComponent();
     }
+
+    private void ReferenceLayer_DragEnter(object sender, DragEventArgs e)
+    {
+        if (!command.Methods.CanExecute(e.Data))
+        {
+            return;
+        }
+
+        ViewModelMain.Current.ActionDisplays[nameof(ReferenceLayer_Drop)] = "Import as reference layer";
+        e.Handled = true;
+    }
+
+    private void ReferenceLayer_DragLeave(object sender, DragEventArgs e)
+    {
+        ViewModelMain.Current.ActionDisplays[nameof(ReferenceLayer_Drop)] = null;
+    }
+
+    private void ReferenceLayer_Drop(object sender, DragEventArgs e)
+    {
+        if (!command.Methods.CanExecute(e.Data))
+        {
+            return;
+        }
+
+        command.Methods.Execute(e.Data);
+        e.Handled = true;
+    }
 }

+ 19 - 8
src/PixiEditor/Views/UserControls/Palettes/PaletteViewer.xaml.cs

@@ -144,35 +144,46 @@ internal partial class PaletteViewer : UserControl
 
     private void Grid_PreviewDragEnter(object sender, DragEventArgs e)
     {
-        if (IsPalFilePresent(e, out _))
+        if (IsSupportedFilePresent(e, out _))
         {
             dragDropGrid.Visibility = Visibility.Visible;
+            ViewModelMain.Current.ActionDisplays[nameof(PaletteViewer)] = "Import palette file";
         }
     }
 
     private void Grid_PreviewDragLeave(object sender, DragEventArgs e)
     {
         dragDropGrid.Visibility = Visibility.Hidden;
+        ViewModelMain.Current.ActionDisplays[nameof(PaletteViewer)] = null;
     }
 
     private async void Grid_Drop(object sender, DragEventArgs e)
     {
-        if (IsPalFilePresent(e, out string filePath))
+        ViewModelMain.Current.ActionDisplays[nameof(PaletteViewer)] = null;
+        
+        if (!IsSupportedFilePresent(e, out string filePath))
         {
-            await ImportPalette(filePath);
-            dragDropGrid.Visibility = Visibility.Hidden;
+            return;
         }
+
+        await ImportPalette(filePath);
+        dragDropGrid.Visibility = Visibility.Hidden;
     }
 
-    private bool IsPalFilePresent(DragEventArgs e, out string filePath)
+    private bool IsSupportedFilePresent(DragEventArgs e, out string filePath)
     {
         if (e.Data.GetDataPresent(DataFormats.FileDrop))
         {
             string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
-            if (files != null && files.Length > 0 && files[0].EndsWith(".pal"))
+            if (files is { Length: > 0 })
             {
-                filePath = files[0];
-                return true;
+                var fileName = files[0];
+                var foundParser = FileParsers.FirstOrDefault(x => x.SupportedFileExtensions.Contains(Path.GetExtension(fileName)));
+                if (foundParser != null)
+                {
+                    filePath = fileName;
+                    return true;
+                }
             }
         }
 

+ 2 - 3
src/PixiEditor/Views/UserControls/PreviewWindow.xaml.cs

@@ -65,7 +65,7 @@ internal partial class PreviewWindow : UserControl
     {
         if (ViewModelMain.Current != null)
         {
-            ViewModelMain.Current.OverrideActionDisplay = false;
+            ViewModelMain.Current.ActionDisplays[nameof(PreviewWindow)] = null;
         }
     }
 
@@ -73,8 +73,7 @@ internal partial class PreviewWindow : UserControl
     {
         if (ViewModelMain.Current != null)
         {
-            ViewModelMain.Current.ActionDisplay = "Right-click to pick color, Shift-right-click to copy color to clipboard";
-            ViewModelMain.Current.OverrideActionDisplay = true;
+            ViewModelMain.Current.ActionDisplays[nameof(PreviewWindow)] = "Right-click to pick color, Shift-right-click to copy color to clipboard";
         }
     }