Explorar o código

Added Layer merging, partialized document and added layer rename shortcut

CPKreuz %!s(int64=4) %!d(string=hai) anos
pai
achega
906d44fe63

+ 1 - 1
PixiEditor/Models/Controllers/BitmapManager.cs

@@ -124,7 +124,7 @@ namespace PixiEditor.Models.Controllers
 
         public WriteableBitmap GetCombinedLayersBitmap()
         {
-            return BitmapUtils.CombineLayers(ActiveDocument.Layers.Where(x => x.IsVisible).ToArray(), ActiveDocument.Width, ActiveDocument.Height);
+            return BitmapUtils.CombineLayers(ActiveDocument.Width, ActiveDocument.Height, ActiveDocument.Layers.Where(x => x.IsVisible).ToArray());
         }
 
         /// <summary>

+ 1 - 1
PixiEditor/Models/Controllers/ClipboardController.cs

@@ -20,7 +20,7 @@ namespace PixiEditor.Models.Controllers
         public static void CopyToClipboard(Layer[] layers, Coordinates[] selection, int originalImageWidth, int originalImageHeight)
         {
             Clipboard.Clear();
-            WriteableBitmap combinedBitmaps = BitmapUtils.CombineLayers(layers, originalImageWidth, originalImageHeight);
+            WriteableBitmap combinedBitmaps = BitmapUtils.CombineLayers(originalImageWidth, originalImageHeight, layers);
             using (MemoryStream pngStream = new MemoryStream())
             {
                 DataObject data = new DataObject();

+ 0 - 696
PixiEditor/Models/DataHolders/Document.cs

@@ -1,696 +0,0 @@
-using System;
-using System.Buffers;
-using System.Collections.ObjectModel;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using PixiEditor.Helpers;
-using PixiEditor.Models.Controllers;
-using PixiEditor.Models.Enums;
-using PixiEditor.Models.ImageManipulation;
-using PixiEditor.Models.IO;
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Position;
-using PixiEditor.Models.Undo;
-using PixiEditor.ViewModels;
-
-namespace PixiEditor.Models.DataHolders
-{
-    public class Document : NotifyableObject
-    {
-        private int activeLayerIndex;
-        private int height;
-        private int width;
-
-        private DateTime openedUtc = DateTime.UtcNow;
-
-        public Document(int width, int height)
-        {
-            Width = width;
-            Height = height;
-            RequestCloseDocumentCommand = new RelayCommand(RequestCloseDocument);
-            SetAsActiveOnClickCommand = new RelayCommand(SetAsActiveOnClick);
-            UndoManager = new UndoManager();
-            XamlAccesibleViewModel = ViewModelMain.Current ?? null;
-            GeneratePreviewLayer();
-            DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(0, 0, width, height));
-        }
-
-        public event EventHandler<DocumentSizeChangedEventArgs> DocumentSizeChanged;
-
-        public event EventHandler<LayersChangedEventArgs> LayersChanged;
-
-        public RelayCommand RequestCloseDocumentCommand { get; set; }
-
-        public RelayCommand SetAsActiveOnClickCommand { get; set; }
-
-        private ViewModelMain xamlAccesibleViewModel = null;
-
-        public ViewModelMain XamlAccesibleViewModel // Used to access ViewModelMain, without changing DataContext in XAML
-        {
-            get => xamlAccesibleViewModel;
-            set
-            {
-                xamlAccesibleViewModel = value;
-                RaisePropertyChanged(nameof(XamlAccesibleViewModel));
-            }
-        }
-
-        private WriteableBitmap previewImage;
-
-        public WriteableBitmap PreviewImage
-        {
-            get => previewImage;
-        }
-
-        private string documentFilePath = string.Empty;
-
-        public string DocumentFilePath
-        {
-            get => documentFilePath;
-            set
-            {
-                documentFilePath = value;
-                RaisePropertyChanged(nameof(DocumentFilePath));
-                RaisePropertyChanged(nameof(Name));
-            }
-        }
-
-        private bool changesSaved = true;
-
-        public bool ChangesSaved
-        {
-            get => changesSaved;
-            set
-            {
-                changesSaved = value;
-                RaisePropertyChanged(nameof(ChangesSaved));
-                RaisePropertyChanged(nameof(Name)); // This updates name so it shows asterisk if unsaved
-            }
-        }
-
-        public string Name
-        {
-            get => (string.IsNullOrEmpty(DocumentFilePath) ? "Untitled" : Path.GetFileName(DocumentFilePath))
-                + (!ChangesSaved ? " *" : string.Empty);
-        }
-
-        public int Width
-        {
-            get => width;
-            set
-            {
-                width = value;
-                RaisePropertyChanged("Width");
-            }
-        }
-
-        public int Height
-        {
-            get => height;
-            set
-            {
-                height = value;
-                RaisePropertyChanged("Height");
-            }
-        }
-
-        public DateTime OpenedUTC
-        {
-            get => openedUtc;
-        }
-
-        private Selection selection = new Selection(Array.Empty<Coordinates>());
-
-        public Selection ActiveSelection
-        {
-            get => selection;
-            set
-            {
-                selection = value;
-                RaisePropertyChanged(nameof(ActiveSelection));
-            }
-        }
-
-        private Layer previewLayer;
-
-        public Layer PreviewLayer
-        {
-            get => previewLayer;
-            set
-            {
-                previewLayer = value;
-                RaisePropertyChanged(nameof(PreviewLayer));
-            }
-        }
-
-        private double mouseXonCanvas;
-
-        private double mouseYonCanvas;
-
-        public double MouseXOnCanvas // Mouse X coordinate relative to canvas
-        {
-            get => mouseXonCanvas;
-            set
-            {
-                mouseXonCanvas = value;
-                RaisePropertyChanged(nameof(MouseXOnCanvas));
-            }
-        }
-
-        public double MouseYOnCanvas // Mouse Y coordinate relative to canvas
-        {
-            get => mouseYonCanvas;
-            set
-            {
-                mouseYonCanvas = value;
-                RaisePropertyChanged(nameof(MouseYOnCanvas));
-            }
-        }
-
-        private double zoomPercentage = 100;
-
-        public double ZoomPercentage
-        {
-            get => zoomPercentage;
-            set
-            {
-                zoomPercentage = value;
-                RaisePropertyChanged(nameof(ZoomPercentage));
-            }
-        }
-
-        private Point viewPortPosition;
-
-        public Point ViewportPosition
-        {
-            get => viewPortPosition;
-            set
-            {
-                viewPortPosition = value;
-                RaisePropertyChanged(nameof(ViewportPosition));
-            }
-        }
-
-        private bool recenterZoombox = true;
-
-        public bool RecenterZoombox
-        {
-            get => recenterZoombox;
-            set
-            {
-                recenterZoombox = value;
-                RaisePropertyChanged(nameof(RecenterZoombox));
-            }
-        }
-
-        public UndoManager UndoManager { get; set; }
-
-        public ObservableCollection<Layer> Layers { get; set; } = new ObservableCollection<Layer>();
-
-        public Layer ActiveLayer => Layers.Count > 0 ? Layers[ActiveLayerIndex] : null;
-
-        public int ActiveLayerIndex
-        {
-            get => activeLayerIndex;
-            set
-            {
-                activeLayerIndex = value;
-                RaisePropertyChanged(nameof(ActiveLayerIndex));
-                RaisePropertyChanged(nameof(ActiveLayer));
-            }
-        }
-
-        public void UpdatePreviewImage()
-        {
-            previewImage = BitmapUtils.GeneratePreviewBitmap(this, 30, 20);
-            RaisePropertyChanged(nameof(PreviewImage));
-        }
-
-        public void GeneratePreviewLayer()
-        {
-            PreviewLayer = new Layer("_previewLayer")
-            {
-                MaxWidth = Width,
-                MaxHeight = Height
-            };
-        }
-
-        public void CenterViewport()
-        {
-            RecenterZoombox = false; // It's a trick to trigger change in UserControl
-            RecenterZoombox = true;
-            ViewportPosition = default;
-            ZoomPercentage = default;
-        }
-
-        public void SaveWithDialog()
-        {
-            bool savedSuccessfully = Exporter.SaveAsEditableFileWithDialog(this, out string path);
-            DocumentFilePath = path;
-            ChangesSaved = savedSuccessfully;
-        }
-
-        public void Save()
-        {
-            Save(DocumentFilePath);
-        }
-
-        public void Save(string path)
-        {
-            DocumentFilePath = Exporter.SaveAsEditableFile(this, path);
-            ChangesSaved = true;
-        }
-
-        public ObservableCollection<Color> Swatches { get; set; } = new ObservableCollection<Color>();
-
-        /// <summary>
-        ///     Resizes canvas to specified width and height to selected anchor.
-        /// </summary>
-        /// <param name="width">New width of canvas.</param>
-        /// <param name="height">New height of canvas.</param>
-        /// <param name="anchor">
-        ///     Point that will act as "starting position" of resizing. Use pipe to connect horizontal and
-        ///     vertical.
-        /// </param>
-        public void ResizeCanvas(int width, int height, AnchorPoint anchor)
-        {
-            int oldWidth = Width;
-            int oldHeight = Height;
-
-            int offsetX = GetOffsetXForAnchor(Width, width, anchor);
-            int offsetY = GetOffsetYForAnchor(Height, height, anchor);
-
-            Thickness[] oldOffsets = Layers.Select(x => x.Offset).ToArray();
-            Thickness[] newOffsets = Layers.Select(x => new Thickness(offsetX + x.OffsetX, offsetY + x.OffsetY, 0, 0))
-                .ToArray();
-
-            object[] processArgs = { newOffsets, width, height };
-            object[] reverseProcessArgs = { oldOffsets, Width, Height };
-
-            ResizeCanvas(newOffsets, width, height);
-            UndoManager.AddUndoChange(new Change(
-                ResizeCanvasProcess,
-                reverseProcessArgs,
-                ResizeCanvasProcess,
-                processArgs,
-                "Resize canvas"));
-            DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(oldWidth, oldHeight, width, height));
-        }
-
-        public void SetActiveLayer(int index)
-        {
-            if (ActiveLayerIndex <= Layers.Count - 1)
-            {
-                ActiveLayer.IsActive = false;
-            }
-
-            if (Layers.Any(x => x.IsActive))
-            {
-                var guids = Layers.Where(x => x.IsActive).Select(y => y.LayerGuid);
-                guids.ToList().ForEach(x => Layers.First(layer => layer.LayerGuid == x).IsActive = false);
-            }
-
-            ActiveLayerIndex = index;
-            ActiveLayer.IsActive = true;
-            LayersChanged?.Invoke(this, new LayersChangedEventArgs(index, LayerAction.SetActive));
-        }
-
-        public void MoveLayerIndexBy(int layerIndex, int amount)
-        {
-            MoveLayerProcess(new object[] { layerIndex, amount });
-
-            UndoManager.AddUndoChange(new Change(
-                MoveLayerProcess,
-                new object[] { layerIndex + amount, -amount },
-                MoveLayerProcess,
-                new object[] { layerIndex, amount },
-                "Move layer"));
-        }
-
-        public void AddNewLayer(string name, WriteableBitmap bitmap, bool setAsActive = true)
-        {
-            AddNewLayer(name, bitmap.PixelWidth, bitmap.PixelHeight, setAsActive);
-            Layers.Last().LayerBitmap = bitmap;
-        }
-
-        public void AddNewLayer(string name, bool setAsActive = true)
-        {
-            AddNewLayer(name, 0, 0, setAsActive);
-        }
-
-        public void AddNewLayer(string name, int width, int height, bool setAsActive = true)
-        {
-            Layers.Add(new Layer(name, width, height)
-            {
-                MaxHeight = Height,
-                MaxWidth = Width
-            });
-            if (setAsActive)
-            {
-                SetActiveLayer(Layers.Count - 1);
-            }
-
-            if (Layers.Count > 1)
-            {
-                StorageBasedChange storageChange = new StorageBasedChange(this, new[] { Layers[^1] }, false);
-                UndoManager.AddUndoChange(
-                    storageChange.ToChange(
-                        RemoveLayerProcess,
-                        new object[] { Layers[^1].LayerGuid },
-                        RestoreLayersProcess,
-                        "Add layer"));
-            }
-
-            LayersChanged?.Invoke(this, new LayersChangedEventArgs(0, LayerAction.Add));
-        }
-
-        public void SetNextLayerAsActive(int lastLayerIndex)
-        {
-            if (Layers.Count > 0)
-            {
-                if (lastLayerIndex == 0)
-                {
-                    SetActiveLayer(0);
-                }
-                else
-                {
-                    SetActiveLayer(lastLayerIndex - 1);
-                }
-            }
-        }
-
-        public void RemoveLayer(int layerIndex)
-        {
-            if (Layers.Count == 0)
-            {
-                return;
-            }
-
-            bool wasActive = Layers[layerIndex].IsActive;
-
-            StorageBasedChange change = new StorageBasedChange(this, new[] { Layers[layerIndex] });
-            UndoManager.AddUndoChange(
-                change.ToChange(RestoreLayersProcess, RemoveLayerProcess, new object[] { Layers[layerIndex].LayerGuid }, "Remove layer"));
-
-            Layers.RemoveAt(layerIndex);
-            if (wasActive)
-            {
-                SetNextLayerAsActive(layerIndex);
-            }
-        }
-
-        /// <summary>
-        ///     Resizes all document layers using NearestNeighbor interpolation.
-        /// </summary>
-        /// <param name="newWidth">New document width.</param>
-        /// <param name="newHeight">New document height.</param>
-        public void Resize(int newWidth, int newHeight)
-        {
-            object[] reverseArgs = { Width, Height };
-            object[] args = { newWidth, newHeight };
-            ResizeDocument(args);
-            UndoManager.AddUndoChange(new Change(
-                ResizeDocument,
-                reverseArgs,
-                ResizeDocument,
-                args,
-                "Resize document"));
-        }
-
-        /// <summary>
-        ///     Resizes canvas, so it fits exactly the size of drawn content, without any transparent pixels outside.
-        /// </summary>
-        public void ClipCanvas()
-        {
-            DoubleCords points = GetEdgePoints();
-            int smallestX = points.Coords1.X;
-            int smallestY = points.Coords1.Y;
-            int biggestX = points.Coords2.X;
-            int biggestY = points.Coords2.Y;
-
-            if (smallestX == 0 && smallestY == 0 && biggestX == 0 && biggestY == 0)
-            {
-                return;
-            }
-
-            int width = biggestX - smallestX;
-            int height = biggestY - smallestY;
-            Coordinates moveVector = new Coordinates(-smallestX, -smallestY);
-
-            Thickness[] oldOffsets = Layers.Select(x => x.Offset).ToArray();
-            int oldWidth = Width;
-            int oldHeight = Height;
-
-            MoveOffsets(moveVector);
-            Width = width;
-            Height = height;
-
-            object[] reverseArguments = { oldOffsets, oldWidth, oldHeight };
-            object[] processArguments = { Layers.Select(x => x.Offset).ToArray(), width, height };
-
-            UndoManager.AddUndoChange(new Change(
-                ResizeCanvasProcess,
-                reverseArguments,
-                ResizeCanvasProcess,
-                processArguments,
-                "Clip canvas"));
-        }
-
-        /// <summary>
-        /// Centers content inside document.
-        /// </summary>
-        public void CenterContent()
-        {
-            DoubleCords points = GetEdgePoints();
-
-            int smallestX = points.Coords1.X;
-            int smallestY = points.Coords1.Y;
-            int biggestX = points.Coords2.X;
-            int biggestY = points.Coords2.Y;
-
-            if (smallestX == 0 && smallestY == 0 && biggestX == 0 && biggestY == 0)
-            {
-                return;
-            }
-
-            Coordinates contentCenter = CoordinatesCalculator.GetCenterPoint(points.Coords1, points.Coords2);
-            Coordinates documentCenter = CoordinatesCalculator.GetCenterPoint(
-                new Coordinates(0, 0),
-                new Coordinates(Width, Height));
-            Coordinates moveVector = new Coordinates(documentCenter.X - contentCenter.X, documentCenter.Y - contentCenter.Y);
-
-            MoveOffsets(moveVector);
-            UndoManager.AddUndoChange(
-                new Change(
-                    MoveOffsetsProcess,
-                    new object[] { new Coordinates(-moveVector.X, -moveVector.Y) },
-                    MoveOffsetsProcess,
-                    new object[] { moveVector },
-                    "Center content"));
-        }
-
-        private void MoveLayerProcess(object[] parameter)
-        {
-            int layerIndex = (int)parameter[0];
-            int amount = (int)parameter[1];
-
-            Layers.Move(layerIndex, layerIndex + amount);
-            if (ActiveLayerIndex == layerIndex)
-            {
-                SetActiveLayer(layerIndex + amount);
-            }
-        }
-
-        private void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
-        {
-            for (int i = 0; i < layers.Length; i++)
-            {
-                Layer layer = layers[i];
-
-                Layers.Insert(layersData[i].LayerIndex, layer);
-                if (layersData[i].IsActive)
-                {
-                    SetActiveLayer(Layers.IndexOf(layer));
-                }
-            }
-        }
-
-        private void RemoveLayerProcess(object[] parameters)
-        {
-            if (parameters != null && parameters.Length > 0 && parameters[0] is Guid layerGuid)
-            {
-                Layer layer = Layers.First(x => x.LayerGuid == layerGuid);
-                int index = Layers.IndexOf(layer);
-                bool wasActive = layer.IsActive;
-                Layers.Remove(layer);
-
-                if (wasActive || ActiveLayerIndex >= index)
-                {
-                    SetNextLayerAsActive(index);
-                }
-            }
-        }
-
-        private void SetAsActiveOnClick(object obj)
-        {
-            XamlAccesibleViewModel.BitmapManager.MouseController.StopRecordingMouseMovementChanges();
-            XamlAccesibleViewModel.BitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
-            if (XamlAccesibleViewModel.BitmapManager.ActiveDocument != this)
-            {
-                XamlAccesibleViewModel.BitmapManager.ActiveDocument = this;
-            }
-        }
-
-        private void RequestCloseDocument(object parameter)
-        {
-            ViewModelMain.Current.DocumentSubViewModel.RequestCloseDocument(this);
-        }
-
-        private int GetOffsetXForAnchor(int srcWidth, int destWidth, AnchorPoint anchor)
-        {
-            if (anchor.HasFlag(AnchorPoint.Center))
-            {
-                return Math.Abs((destWidth / 2) - (srcWidth / 2));
-            }
-
-            if (anchor.HasFlag(AnchorPoint.Right))
-            {
-                return Math.Abs(destWidth - srcWidth);
-            }
-
-            return 0;
-        }
-
-        private int GetOffsetYForAnchor(int srcHeight, int destHeight, AnchorPoint anchor)
-        {
-            if (anchor.HasFlag(AnchorPoint.Middle))
-            {
-                return Math.Abs((destHeight / 2) - (srcHeight / 2));
-            }
-
-            if (anchor.HasFlag(AnchorPoint.Bottom))
-            {
-                return Math.Abs(destHeight - srcHeight);
-            }
-
-            return 0;
-        }
-
-        private void ResizeDocument(object[] arguments)
-        {
-            int oldWidth = Width;
-            int oldHeight = Height;
-
-            int newWidth = (int)arguments[0];
-            int newHeight = (int)arguments[1];
-
-            for (int i = 0; i < Layers.Count; i++)
-            {
-                float widthRatio = (float)newWidth / Width;
-                float heightRatio = (float)newHeight / Height;
-                int layerWidth = (int)(Layers[i].Width * widthRatio);
-                int layerHeight = (int)(Layers[i].Height * heightRatio);
-
-                Layers[i].Resize(layerWidth, layerHeight, newWidth, newHeight);
-                Layers[i].Offset = new Thickness(Math.Floor(Layers[i].OffsetX * widthRatio), Math.Floor(Layers[i].OffsetY * heightRatio), 0, 0);
-            }
-
-            Height = newHeight;
-            Width = newWidth;
-            DocumentSizeChanged?.Invoke(
-                this,
-                new DocumentSizeChangedEventArgs(oldWidth, oldHeight, newWidth, newHeight));
-        }
-
-        private void ResizeCanvasProcess(object[] arguments)
-        {
-            int oldWidth = Width;
-            int oldHeight = Height;
-
-            Thickness[] offset = (Thickness[])arguments[0];
-            int width = (int)arguments[1];
-            int height = (int)arguments[2];
-            ResizeCanvas(offset, width, height);
-            DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(oldWidth, oldHeight, width, height));
-        }
-
-        /// <summary>
-        ///     Resizes canvas.
-        /// </summary>
-        /// <param name="offset">Offset of content in new canvas. It will move layer to that offset.</param>
-        /// <param name="newWidth">New canvas size.</param>
-        /// <param name="newHeight">New canvas height.</param>
-        private void ResizeCanvas(Thickness[] offset, int newWidth, int newHeight)
-        {
-            for (int i = 0; i < Layers.Count; i++)
-            {
-                Layers[i].Offset = offset[i];
-                Layers[i].MaxWidth = newWidth;
-                Layers[i].MaxHeight = newHeight;
-            }
-
-            Width = newWidth;
-            Height = newHeight;
-        }
-
-        private DoubleCords GetEdgePoints()
-        {
-            Layer firstLayer = Layers[0];
-            int smallestX = firstLayer.OffsetX;
-            int smallestY = firstLayer.OffsetY;
-            int biggestX = smallestX + firstLayer.Width;
-            int biggestY = smallestY + firstLayer.Height;
-
-            for (int i = 0; i < Layers.Count; i++)
-            {
-                Layers[i].ClipCanvas();
-                if (Layers[i].OffsetX < smallestX)
-                {
-                    smallestX = Layers[i].OffsetX;
-                }
-
-                if (Layers[i].OffsetX + Layers[i].Width > biggestX)
-                {
-                    biggestX = Layers[i].OffsetX + Layers[i].Width;
-                }
-
-                if (Layers[i].OffsetY < smallestY)
-                {
-                    smallestY = Layers[i].OffsetY;
-                }
-
-                if (Layers[i].OffsetY + Layers[i].Height > biggestY)
-                {
-                    biggestY = Layers[i].OffsetY + Layers[i].Height;
-                }
-            }
-
-            return new DoubleCords(
-                new Coordinates(smallestX, smallestY),
-                new Coordinates(biggestX, biggestY));
-        }
-
-        /// <summary>
-        ///     Moves offsets of layers by specified vector.
-        /// </summary>
-        private void MoveOffsets(Coordinates moveVector)
-        {
-            for (int i = 0; i < Layers.Count; i++)
-            {
-                Thickness offset = Layers[i].Offset;
-                Layers[i].Offset = new Thickness(offset.Left + moveVector.X, offset.Top + moveVector.Y, 0, 0);
-            }
-        }
-
-        private void MoveOffsetsProcess(object[] arguments)
-        {
-            Coordinates vector = (Coordinates)arguments[0];
-            MoveOffsets(vector);
-        }
-    }
-}

+ 17 - 0
PixiEditor/Models/DataHolders/Document/Document.Commands.cs

@@ -0,0 +1,17 @@
+using PixiEditor.Helpers;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public partial class Document
+    {
+        public RelayCommand RequestCloseDocumentCommand { get; set; }
+
+        public RelayCommand SetAsActiveOnClickCommand { get; set; }
+
+        private void SetRelayCommands()
+        {
+            RequestCloseDocumentCommand = new RelayCommand(RequestCloseDocument);
+            SetAsActiveOnClickCommand = new RelayCommand(SetAsActiveOnClick);
+        }
+    }
+}

+ 24 - 0
PixiEditor/Models/DataHolders/Document/Document.Constructors.cs

@@ -0,0 +1,24 @@
+using PixiEditor.Models.Controllers;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public partial class Document
+    {
+        public Document(int width, int height)
+            : this()
+        {
+            Width = width;
+            Height = height;
+            DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(0, 0, width, height));
+        }
+
+        private Document()
+        {
+            SetRelayCommands();
+            UndoManager = new UndoManager();
+            XamlAccesibleViewModel = ViewModelMain.Current ?? null;
+            GeneratePreviewLayer();
+        }
+    }
+}

+ 18 - 0
PixiEditor/Models/DataHolders/Document/Document.Discord.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public partial class Document
+    {
+        private readonly DateTime openedUtc = DateTime.UtcNow;
+
+        public DateTime OpenedUTC
+        {
+            get => openedUtc;
+        }
+    }
+}

+ 51 - 0
PixiEditor/Models/DataHolders/Document/Document.IO.cs

@@ -0,0 +1,51 @@
+using PixiEditor.Models.IO;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public partial class Document
+    {
+        private string documentFilePath = string.Empty;
+
+        public string DocumentFilePath
+        {
+            get => documentFilePath;
+            set
+            {
+                documentFilePath = value;
+                RaisePropertyChanged(nameof(DocumentFilePath));
+                RaisePropertyChanged(nameof(Name));
+            }
+        }
+
+        private bool changesSaved = true;
+
+        public bool ChangesSaved
+        {
+            get => changesSaved;
+            set
+            {
+                changesSaved = value;
+                RaisePropertyChanged(nameof(ChangesSaved));
+                RaisePropertyChanged(nameof(Name)); // This updates name so it shows asterisk if unsaved
+            }
+        }
+
+        public void SaveWithDialog()
+        {
+            bool savedSuccessfully = Exporter.SaveAsEditableFileWithDialog(this, out string path);
+            DocumentFilePath = path;
+            ChangesSaved = savedSuccessfully;
+        }
+
+        public void Save()
+        {
+            Save(DocumentFilePath);
+        }
+
+        public void Save(string path)
+        {
+            DocumentFilePath = Exporter.SaveAsEditableFile(this, path);
+            ChangesSaved = true;
+        }
+    }
+}

+ 243 - 0
PixiEditor/Models/DataHolders/Document/Document.Layers.cs

@@ -0,0 +1,243 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public partial class Document
+    {
+        private int activeLayerIndex;
+
+        public ObservableCollection<Layer> Layers { get; set; } = new ObservableCollection<Layer>();
+
+        public Layer ActiveLayer => Layers.Count > 0 ? Layers[ActiveLayerIndex] : null;
+
+        public int ActiveLayerIndex
+        {
+            get => activeLayerIndex;
+            set
+            {
+                activeLayerIndex = value;
+                RaisePropertyChanged(nameof(ActiveLayerIndex));
+                RaisePropertyChanged(nameof(ActiveLayer));
+            }
+        }
+
+        public event EventHandler<LayersChangedEventArgs> LayersChanged;
+
+        public void SetActiveLayer(int index)
+        {
+            if (ActiveLayerIndex <= Layers.Count - 1)
+            {
+                ActiveLayer.IsActive = false;
+            }
+
+            if (Layers.Any(x => x.IsActive))
+            {
+                var guids = Layers.Where(x => x.IsActive).Select(y => y.LayerGuid);
+                guids.ToList().ForEach(x => Layers.First(layer => layer.LayerGuid == x).IsActive = false);
+            }
+
+            ActiveLayerIndex = index;
+            ActiveLayer.IsActive = true;
+            LayersChanged?.Invoke(this, new LayersChangedEventArgs(index, LayerAction.SetActive));
+        }
+
+        public void MoveLayerIndexBy(int layerIndex, int amount)
+        {
+            MoveLayerProcess(new object[] { layerIndex, amount });
+
+            UndoManager.AddUndoChange(new Change(
+                MoveLayerProcess,
+                new object[] { layerIndex + amount, -amount },
+                MoveLayerProcess,
+                new object[] { layerIndex, amount },
+                "Move layer"));
+        }
+
+        public void AddNewLayer(string name, WriteableBitmap bitmap, bool setAsActive = true)
+        {
+            AddNewLayer(name, bitmap.PixelWidth, bitmap.PixelHeight, setAsActive);
+            Layers.Last().LayerBitmap = bitmap;
+        }
+
+        public void AddNewLayer(string name, bool setAsActive = true)
+        {
+            AddNewLayer(name, 0, 0, setAsActive);
+        }
+
+        public void AddNewLayer(string name, int width, int height, bool setAsActive = true)
+        {
+            Layers.Add(new Layer(name, width, height)
+            {
+                MaxHeight = Height,
+                MaxWidth = Width
+            });
+            if (setAsActive)
+            {
+                SetActiveLayer(Layers.Count - 1);
+            }
+
+            if (Layers.Count > 1)
+            {
+                StorageBasedChange storageChange = new StorageBasedChange(this, new[] { Layers[^1] }, false);
+                UndoManager.AddUndoChange(
+                    storageChange.ToChange(
+                        RemoveLayerProcess,
+                        new object[] { Layers[^1].LayerGuid },
+                        RestoreLayersProcess,
+                        "Add layer"));
+            }
+
+            LayersChanged?.Invoke(this, new LayersChangedEventArgs(0, LayerAction.Add));
+        }
+
+        public void SetNextLayerAsActive(int lastLayerIndex)
+        {
+            if (Layers.Count > 0)
+            {
+                if (lastLayerIndex == 0)
+                {
+                    SetActiveLayer(0);
+                }
+                else
+                {
+                    SetActiveLayer(lastLayerIndex - 1);
+                }
+            }
+        }
+
+        public void RemoveLayer(int layerIndex)
+        {
+            if (Layers.Count == 0)
+            {
+                return;
+            }
+
+            bool wasActive = Layers[layerIndex].IsActive;
+
+            StorageBasedChange change = new StorageBasedChange(this, new[] { Layers[layerIndex] });
+            UndoManager.AddUndoChange(
+                change.ToChange(RestoreLayersProcess, RemoveLayerProcess, new object[] { Layers[layerIndex].LayerGuid }, "Remove layer"));
+
+            Layers.RemoveAt(layerIndex);
+            if (wasActive)
+            {
+                SetNextLayerAsActive(layerIndex);
+            }
+        }
+
+        /// <summary>
+        /// Merges two layers.
+        /// </summary>
+        /// <param name="firstLayer">The index of the first.</param>
+        /// <param name="secondLayer">The index of the second.</param>
+        /// <returns>The merged layer.</returns>
+        public Layer MergeLayers(Layer firstLayer, Layer secondLayer, bool nameOfSecond, int index)
+        {
+            string name;
+
+            // Wich name should be user
+            if (nameOfSecond)
+            {
+                name = secondLayer.Name;
+            }
+            else
+            {
+                name = firstLayer.Name;
+            }
+
+            Layer mergedLayer = firstLayer.MergeWith(secondLayer, name, Width, Height);
+
+            // Insert new layer and remove old
+            Layers.Insert(index, mergedLayer);
+            Layers.Remove(firstLayer);
+            Layers.Remove(secondLayer);
+
+            SetActiveLayer(Layers.IndexOf(mergedLayer));
+
+            return mergedLayer;
+        }
+
+        /// <summary>
+        /// Merges two layers.
+        /// </summary>
+        /// <param name="firstIndex">The index of the first.</param>
+        /// <param name="secondIndex">The index of the second.</param>
+        /// <returns>The merged layer.</returns>
+        public Layer MergeLayers(int firstIndex, int secondIndex, bool nameOfSecond)
+        {
+            Layer firstLayer = Layers[firstIndex];
+            Layer secondLayer = Layers[secondIndex];
+
+            return MergeLayers(firstLayer, secondLayer, nameOfSecond, firstIndex);
+        }
+
+        /// <summary>
+        ///     Moves offsets of layers by specified vector.
+        /// </summary>
+        private void MoveOffsets(Coordinates moveVector)
+        {
+            for (int i = 0; i < Layers.Count; i++)
+            {
+                Thickness offset = Layers[i].Offset;
+                Layers[i].Offset = new Thickness(offset.Left + moveVector.X, offset.Top + moveVector.Y, 0, 0);
+            }
+        }
+
+        private void MoveOffsetsProcess(object[] arguments)
+        {
+            Coordinates vector = (Coordinates)arguments[0];
+            MoveOffsets(vector);
+        }
+
+        private void MoveLayerProcess(object[] parameter)
+        {
+            int layerIndex = (int)parameter[0];
+            int amount = (int)parameter[1];
+
+            Layers.Move(layerIndex, layerIndex + amount);
+            if (ActiveLayerIndex == layerIndex)
+            {
+                SetActiveLayer(layerIndex + amount);
+            }
+        }
+
+        private void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
+        {
+            for (int i = 0; i < layers.Length; i++)
+            {
+                Layer layer = layers[i];
+
+                Layers.Insert(layersData[i].LayerIndex, layer);
+                if (layersData[i].IsActive)
+                {
+                    SetActiveLayer(Layers.IndexOf(layer));
+                }
+            }
+        }
+
+        private void RemoveLayerProcess(object[] parameters)
+        {
+            if (parameters != null && parameters.Length > 0 && parameters[0] is Guid layerGuid)
+            {
+                Layer layer = Layers.First(x => x.LayerGuid == layerGuid);
+                int index = Layers.IndexOf(layer);
+                bool wasActive = layer.IsActive;
+                Layers.Remove(layer);
+
+                if (wasActive || ActiveLayerIndex >= index)
+                {
+                    SetNextLayerAsActive(index);
+                }
+            }
+        }
+    }
+}

+ 125 - 0
PixiEditor/Models/DataHolders/Document/Document.Operations.cs

@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.Undo;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public partial class Document
+    {
+        public event EventHandler<DocumentSizeChangedEventArgs> DocumentSizeChanged;
+
+        /// <summary>
+        ///     Resizes canvas.
+        /// </summary>
+        /// <param name="offset">Offset of content in new canvas. It will move layer to that offset.</param>
+        /// <param name="newWidth">New canvas size.</param>
+        /// <param name="newHeight">New canvas height.</param>
+        private void ResizeCanvas(Thickness[] offset, int newWidth, int newHeight)
+        {
+            for (int i = 0; i < Layers.Count; i++)
+            {
+                Layers[i].Offset = offset[i];
+                Layers[i].MaxWidth = newWidth;
+                Layers[i].MaxHeight = newHeight;
+            }
+
+            Width = newWidth;
+            Height = newHeight;
+        }
+
+        /// <summary>
+        ///     Resizes canvas to specified width and height to selected anchor.
+        /// </summary>
+        /// <param name="width">New width of canvas.</param>
+        /// <param name="height">New height of canvas.</param>
+        /// <param name="anchor">
+        ///     Point that will act as "starting position" of resizing. Use pipe to connect horizontal and
+        ///     vertical.
+        /// </param>
+        public void ResizeCanvas(int width, int height, AnchorPoint anchor)
+        {
+            int oldWidth = Width;
+            int oldHeight = Height;
+
+            int offsetX = GetOffsetXForAnchor(Width, width, anchor);
+            int offsetY = GetOffsetYForAnchor(Height, height, anchor);
+
+            Thickness[] oldOffsets = Layers.Select(x => x.Offset).ToArray();
+            Thickness[] newOffsets = Layers.Select(x => new Thickness(offsetX + x.OffsetX, offsetY + x.OffsetY, 0, 0))
+                .ToArray();
+
+            object[] processArgs = { newOffsets, width, height };
+            object[] reverseProcessArgs = { oldOffsets, Width, Height };
+
+            ResizeCanvas(newOffsets, width, height);
+            UndoManager.AddUndoChange(new Change(
+                ResizeCanvasProcess,
+                reverseProcessArgs,
+                ResizeCanvasProcess,
+                processArgs,
+                "Resize canvas"));
+            DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(oldWidth, oldHeight, width, height));
+        }
+
+        /// <summary>
+        ///     Resizes all document layers using NearestNeighbor interpolation.
+        /// </summary>
+        /// <param name="newWidth">New document width.</param>
+        /// <param name="newHeight">New document height.</param>
+        public void Resize(int newWidth, int newHeight)
+        {
+            object[] reverseArgs = { Width, Height };
+            object[] args = { newWidth, newHeight };
+            ResizeDocument(args);
+            UndoManager.AddUndoChange(new Change(
+                ResizeDocument,
+                reverseArgs,
+                ResizeDocument,
+                args,
+                "Resize document"));
+        }
+
+        private void ResizeDocument(object[] arguments)
+        {
+            int oldWidth = Width;
+            int oldHeight = Height;
+
+            int newWidth = (int)arguments[0];
+            int newHeight = (int)arguments[1];
+
+            for (int i = 0; i < Layers.Count; i++)
+            {
+                float widthRatio = (float)newWidth / Width;
+                float heightRatio = (float)newHeight / Height;
+                int layerWidth = (int)(Layers[i].Width * widthRatio);
+                int layerHeight = (int)(Layers[i].Height * heightRatio);
+
+                Layers[i].Resize(layerWidth, layerHeight, newWidth, newHeight);
+                Layers[i].Offset = new Thickness(Math.Floor(Layers[i].OffsetX * widthRatio), Math.Floor(Layers[i].OffsetY * heightRatio), 0, 0);
+            }
+
+            Height = newHeight;
+            Width = newWidth;
+            DocumentSizeChanged?.Invoke(
+                this,
+                new DocumentSizeChangedEventArgs(oldWidth, oldHeight, newWidth, newHeight));
+        }
+
+        private void ResizeCanvasProcess(object[] arguments)
+        {
+            int oldWidth = Width;
+            int oldHeight = Height;
+
+            Thickness[] offset = (Thickness[])arguments[0];
+            int width = (int)arguments[1];
+            int height = (int)arguments[2];
+            ResizeCanvas(offset, width, height);
+            DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(oldWidth, oldHeight, width, height));
+        }
+    }
+}

+ 44 - 0
PixiEditor/Models/DataHolders/Document/Document.Preview.cs

@@ -0,0 +1,44 @@
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Layers;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public partial class Document
+    {
+        private WriteableBitmap previewImage;
+
+        public WriteableBitmap PreviewImage
+        {
+            get => previewImage;
+        }
+
+        private Layer previewLayer;
+
+        public Layer PreviewLayer
+        {
+            get => previewLayer;
+            set
+            {
+                previewLayer = value;
+                RaisePropertyChanged(nameof(PreviewLayer));
+            }
+        }
+
+        public void UpdatePreviewImage()
+        {
+            previewImage = BitmapUtils.GeneratePreviewBitmap(this, 30, 20);
+            RaisePropertyChanged(nameof(PreviewImage));
+        }
+
+        public void GeneratePreviewLayer()
+        {
+            PreviewLayer = new Layer("_previewLayer")
+            {
+                MaxWidth = Width,
+                MaxHeight = Height
+            };
+        }
+
+    }
+}

+ 304 - 0
PixiEditor/Models/DataHolders/Document/Document.cs

@@ -0,0 +1,304 @@
+using System;
+using System.Buffers;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public partial class Document : NotifyableObject
+    {
+        private int height;
+        private int width;
+
+        private ViewModelMain xamlAccesibleViewModel = null;
+
+        public ViewModelMain XamlAccesibleViewModel // Used to access ViewModelMain, without changing DataContext in XAML
+        {
+            get => xamlAccesibleViewModel;
+            set
+            {
+                xamlAccesibleViewModel = value;
+                RaisePropertyChanged(nameof(XamlAccesibleViewModel));
+            }
+        }
+
+        public string Name
+        {
+            get => (string.IsNullOrEmpty(DocumentFilePath) ? "Untitled" : Path.GetFileName(DocumentFilePath))
+                + (!ChangesSaved ? " *" : string.Empty);
+        }
+
+        public int Width
+        {
+            get => width;
+            set
+            {
+                width = value;
+                RaisePropertyChanged("Width");
+            }
+        }
+
+        public int Height
+        {
+            get => height;
+            set
+            {
+                height = value;
+                RaisePropertyChanged("Height");
+            }
+        }
+
+        private Selection selection = new Selection(Array.Empty<Coordinates>());
+
+        public Selection ActiveSelection
+        {
+            get => selection;
+            set
+            {
+                selection = value;
+                RaisePropertyChanged(nameof(ActiveSelection));
+            }
+        }
+
+        private double mouseXonCanvas;
+
+        private double mouseYonCanvas;
+
+        public double MouseXOnCanvas // Mouse X coordinate relative to canvas
+        {
+            get => mouseXonCanvas;
+            set
+            {
+                mouseXonCanvas = value;
+                RaisePropertyChanged(nameof(MouseXOnCanvas));
+            }
+        }
+
+        public double MouseYOnCanvas // Mouse Y coordinate relative to canvas
+        {
+            get => mouseYonCanvas;
+            set
+            {
+                mouseYonCanvas = value;
+                RaisePropertyChanged(nameof(MouseYOnCanvas));
+            }
+        }
+
+        private double zoomPercentage = 100;
+
+        public double ZoomPercentage
+        {
+            get => zoomPercentage;
+            set
+            {
+                zoomPercentage = value;
+                RaisePropertyChanged(nameof(ZoomPercentage));
+            }
+        }
+
+        private Point viewPortPosition;
+
+        public Point ViewportPosition
+        {
+            get => viewPortPosition;
+            set
+            {
+                viewPortPosition = value;
+                RaisePropertyChanged(nameof(ViewportPosition));
+            }
+        }
+
+        private bool recenterZoombox = true;
+
+        public bool RecenterZoombox
+        {
+            get => recenterZoombox;
+            set
+            {
+                recenterZoombox = value;
+                RaisePropertyChanged(nameof(RecenterZoombox));
+            }
+        }
+
+        public UndoManager UndoManager { get; set; }
+
+        public void CenterViewport()
+        {
+            RecenterZoombox = false; // It's a trick to trigger change in UserControl
+            RecenterZoombox = true;
+            ViewportPosition = default;
+            ZoomPercentage = default;
+        }
+
+        public ObservableCollection<Color> Swatches { get; set; } = new ObservableCollection<Color>();
+
+        /// <summary>
+        ///     Resizes canvas, so it fits exactly the size of drawn content, without any transparent pixels outside.
+        /// </summary>
+        public void ClipCanvas()
+        {
+            DoubleCords points = GetEdgePoints();
+            int smallestX = points.Coords1.X;
+            int smallestY = points.Coords1.Y;
+            int biggestX = points.Coords2.X;
+            int biggestY = points.Coords2.Y;
+
+            if (smallestX == 0 && smallestY == 0 && biggestX == 0 && biggestY == 0)
+            {
+                return;
+            }
+
+            int width = biggestX - smallestX;
+            int height = biggestY - smallestY;
+            Coordinates moveVector = new Coordinates(-smallestX, -smallestY);
+
+            Thickness[] oldOffsets = Layers.Select(x => x.Offset).ToArray();
+            int oldWidth = Width;
+            int oldHeight = Height;
+
+            MoveOffsets(moveVector);
+            Width = width;
+            Height = height;
+
+            object[] reverseArguments = { oldOffsets, oldWidth, oldHeight };
+            object[] processArguments = { Layers.Select(x => x.Offset).ToArray(), width, height };
+
+            UndoManager.AddUndoChange(new Change(
+                ResizeCanvasProcess,
+                reverseArguments,
+                ResizeCanvasProcess,
+                processArguments,
+                "Clip canvas"));
+        }
+
+        /// <summary>
+        /// Centers content inside document.
+        /// </summary>
+        public void CenterContent()
+        {
+            DoubleCords points = GetEdgePoints();
+
+            int smallestX = points.Coords1.X;
+            int smallestY = points.Coords1.Y;
+            int biggestX = points.Coords2.X;
+            int biggestY = points.Coords2.Y;
+
+            if (smallestX == 0 && smallestY == 0 && biggestX == 0 && biggestY == 0)
+            {
+                return;
+            }
+
+            Coordinates contentCenter = CoordinatesCalculator.GetCenterPoint(points.Coords1, points.Coords2);
+            Coordinates documentCenter = CoordinatesCalculator.GetCenterPoint(
+                new Coordinates(0, 0),
+                new Coordinates(Width, Height));
+            Coordinates moveVector = new Coordinates(documentCenter.X - contentCenter.X, documentCenter.Y - contentCenter.Y);
+
+            MoveOffsets(moveVector);
+            UndoManager.AddUndoChange(
+                new Change(
+                    MoveOffsetsProcess,
+                    new object[] { new Coordinates(-moveVector.X, -moveVector.Y) },
+                    MoveOffsetsProcess,
+                    new object[] { moveVector },
+                    "Center content"));
+        }
+
+        private void SetAsActiveOnClick(object obj)
+        {
+            XamlAccesibleViewModel.BitmapManager.MouseController.StopRecordingMouseMovementChanges();
+            XamlAccesibleViewModel.BitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
+            if (XamlAccesibleViewModel.BitmapManager.ActiveDocument != this)
+            {
+                XamlAccesibleViewModel.BitmapManager.ActiveDocument = this;
+            }
+        }
+
+        private void RequestCloseDocument(object parameter)
+        {
+            ViewModelMain.Current.DocumentSubViewModel.RequestCloseDocument(this);
+        }
+
+        private int GetOffsetXForAnchor(int srcWidth, int destWidth, AnchorPoint anchor)
+        {
+            if (anchor.HasFlag(AnchorPoint.Center))
+            {
+                return Math.Abs((destWidth / 2) - (srcWidth / 2));
+            }
+
+            if (anchor.HasFlag(AnchorPoint.Right))
+            {
+                return Math.Abs(destWidth - srcWidth);
+            }
+
+            return 0;
+        }
+
+        private int GetOffsetYForAnchor(int srcHeight, int destHeight, AnchorPoint anchor)
+        {
+            if (anchor.HasFlag(AnchorPoint.Middle))
+            {
+                return Math.Abs((destHeight / 2) - (srcHeight / 2));
+            }
+
+            if (anchor.HasFlag(AnchorPoint.Bottom))
+            {
+                return Math.Abs(destHeight - srcHeight);
+            }
+
+            return 0;
+        }
+
+        private DoubleCords GetEdgePoints()
+        {
+            Layer firstLayer = Layers[0];
+            int smallestX = firstLayer.OffsetX;
+            int smallestY = firstLayer.OffsetY;
+            int biggestX = smallestX + firstLayer.Width;
+            int biggestY = smallestY + firstLayer.Height;
+
+            for (int i = 0; i < Layers.Count; i++)
+            {
+                Layers[i].ClipCanvas();
+                if (Layers[i].OffsetX < smallestX)
+                {
+                    smallestX = Layers[i].OffsetX;
+                }
+
+                if (Layers[i].OffsetX + Layers[i].Width > biggestX)
+                {
+                    biggestX = Layers[i].OffsetX + Layers[i].Width;
+                }
+
+                if (Layers[i].OffsetY < smallestY)
+                {
+                    smallestY = Layers[i].OffsetY;
+                }
+
+                if (Layers[i].OffsetY + Layers[i].Height > biggestY)
+                {
+                    biggestY = Layers[i].OffsetY + Layers[i].Height;
+                }
+            }
+
+            return new DoubleCords(
+                new Coordinates(smallestX, smallestY),
+                new Coordinates(biggestX, biggestY));
+        }
+    }
+}

+ 2 - 2
PixiEditor/Models/ImageManipulation/BitmapUtils.cs

@@ -35,9 +35,9 @@ namespace PixiEditor.Models.ImageManipulation
         /// </summary>
         /// <param name="layers">Layers to combine.</param>
         /// <param name="width">Width of final bitmap.</param>
-        /// <param name="height">Height of final bitmap.</param>        
+        /// <param name="height">Height of final bitmap.</param>.
         /// <returns>WriteableBitmap of layered bitmaps.</returns>
-        public static WriteableBitmap CombineLayers(Layer[] layers, int width, int height)
+        public static WriteableBitmap CombineLayers(int width, int height, params Layer[] layers)
         {
             WriteableBitmap finalBitmap = BitmapFactory.New(width, height);
 

+ 70 - 5
PixiEditor/Models/Layers/LayerHelper.cs

@@ -1,19 +1,22 @@
 using System;
 using System.Linq;
+using System.Windows;
+using System.Windows.Media.Imaging;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.ImageManipulation;
 using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Layers
 {
     public static class LayerHelper
     {
-         public static Layer FindLayerByGuid(Document document, Guid guid)
-         {
+        public static Layer FindLayerByGuid(Document document, Guid guid)
+        {
             return document.Layers.FirstOrDefault(x => x.LayerGuid == guid);
-         }
+        }
 
-         public static object FindLayerByGuidProcess(object[] parameters)
-         {
+        public static object FindLayerByGuidProcess(object[] parameters)
+        {
             if (parameters != null && parameters.Length > 0 && parameters[0] is Guid guid)
             {
                 return FindLayerByGuid(ViewModelMain.Current.BitmapManager.ActiveDocument, guid);
@@ -21,5 +24,67 @@ namespace PixiEditor.Models.Layers
 
             return null;
         }
+
+        /// <summary>
+        /// Gets the closer layers to the axises.
+        /// </summary>
+        /// <param name="xCloser">The layer closer to the x Axis.</param>
+        /// <param name="yCloser">The layer closer to the y Axis.</param>
+        /// <param name="xOther">The other layer that is not closer to the x axis.</param>
+        /// <param name="yOther">The other layer that is not closer to the y axis.</param>
+        public static void GetCloser(this Layer layer1, Layer layer2, out Layer xCloser, out Layer yCloser, out Layer xOther, out Layer yOther)
+        {
+            if (layer2.OffsetX > layer1.OffsetX)
+            {
+                xCloser = layer1;
+                xOther = layer2;
+            }
+            else
+            {
+                xCloser = layer2;
+                xOther = layer1;
+            }
+
+            if (layer2.OffsetY > layer1.OffsetY)
+            {
+                yCloser = layer1;
+                yOther = layer2;
+            }
+            else
+            {
+                yCloser = layer2;
+                yOther = layer1;
+            }
+        }
+
+        public static Layer MergeWith(this Layer thisLayer, Layer otherLayer, string newName, Vector documentsSize)
+        {
+            thisLayer.GetCloser(otherLayer, out Layer xCloser, out Layer yCloser, out Layer xOther, out Layer yOther);
+
+            // Calculate the offset to the other layer
+            int offsetX = Math.Abs(xCloser.OffsetX + xCloser.Width - xOther.OffsetX);
+            int offsetY = Math.Abs(yCloser.OffsetY + yCloser.Height - yOther.OffsetY);
+
+            // Calculate the needed width and height of the new layer
+            int width = xCloser.Width + offsetX + xOther.Width;
+            int height = yCloser.Height + offsetY + yOther.Height;
+
+            // Merge both layers into a bitmap
+            WriteableBitmap mergedBitmap = BitmapUtils.CombineLayers((int)documentsSize.X, (int)documentsSize.Y, thisLayer, otherLayer);
+            mergedBitmap = mergedBitmap.Crop(xCloser.OffsetX, yCloser.OffsetY, width, height);
+
+            // Create the new layer with the merged bitmap
+            Layer mergedLayer = new Layer(newName, mergedBitmap)
+            {
+                Offset = new Thickness(xCloser.OffsetX, yCloser.OffsetY, 0, 0)
+            };
+
+            return mergedLayer;
+        }
+
+        public static Layer MergeWith(this Layer thisLayer, Layer otherLayer, string newName, int documentWidth, int documentHeight)
+        {
+            return MergeWith(thisLayer, otherLayer, newName, new Vector(documentWidth, documentHeight));
+        }
     }
 }

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

@@ -16,6 +16,10 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public RelayCommand MoveToFrontCommand { get; set; }
 
+        public RelayCommand MergeWithAboveCommand { get; set; }
+
+        public RelayCommand MergeWithBelowCommand { get; set; }
+
         public LayersViewModel(ViewModelMain owner)
             : base(owner)
         {
@@ -25,6 +29,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             MoveToBackCommand = new RelayCommand(MoveLayerToBack, CanMoveToBack);
             MoveToFrontCommand = new RelayCommand(MoveLayerToFront, CanMoveToFront);
             RenameLayerCommand = new RelayCommand(RenameLayer);
+            MergeWithAboveCommand = new RelayCommand(MergeWithAbove, CanMergeWithAbove);
+            MergeWithBelowCommand = new RelayCommand(MergeWithBelow, CanMergeWithBelow);
         }
 
         public void NewLayer(object parameter)
@@ -54,7 +60,19 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public void RenameLayer(object parameter)
         {
-            Owner.BitmapManager.ActiveDocument.Layers[(int)parameter].IsRenaming = true;
+            int? index = (int?)parameter;
+
+            if (index == null)
+            {
+                index = Owner.BitmapManager.ActiveDocument.ActiveLayerIndex;
+            }
+
+            Owner.BitmapManager.ActiveDocument.Layers[index.Value].IsRenaming = true;
+        }
+
+        public bool CanRenameLayer(object parameter)
+        {
+            return Owner.BitmapManager.ActiveDocument != null;
         }
 
         public void MoveLayerToFront(object parameter)
@@ -78,5 +96,29 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             return (int)property > 0;
         }
+
+        public void MergeWithAbove(object parameter)
+        {
+            int index = (int)parameter;
+            Owner.BitmapManager.ActiveDocument.MergeLayers(index, index + 1, false);
+        }
+
+        public void MergeWithBelow(object parameter)
+        {
+            int index = (int)parameter;
+            Owner.BitmapManager.ActiveDocument.MergeLayers(index, index - 1, true);
+        }
+
+        public bool CanMergeWithAbove(object propery)
+        {
+            int index = (int)propery;
+            return Owner.DocumentIsNotNull(null) && index != Owner.BitmapManager.ActiveDocument.Layers.Count - 1;
+        }
+
+        public bool CanMergeWithBelow(object propery)
+        {
+            int index = (int)propery;
+            return Owner.DocumentIsNotNull(null) && index != 0;
+        }
     }
 }

+ 4 - 1
PixiEditor/ViewModels/ViewModelMain.cs

@@ -134,7 +134,10 @@ namespace PixiEditor.ViewModels
                     new Shortcut(Key.S, FileSubViewModel.ExportFileCommand, modifier: ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt),
                     new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, modifier: ModifierKeys.Control),
                     new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "AsNew", ModifierKeys.Control | ModifierKeys.Shift),
-                    new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, modifier: ModifierKeys.Control)
+                    new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, modifier: ModifierKeys.Control),
+
+                    // Layers
+                    new Shortcut(Key.F2, LayersSubViewModel.RenameLayerCommand, BitmapManager.ActiveDocument?.ActiveLayerIndex)
                 }
             };
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;

+ 8 - 0
PixiEditor/Views/MainWindow.xaml

@@ -334,6 +334,14 @@
                                                                         <MenuItem Header="Move to back"
                                                                                   Command="{Binding LayersSubViewModel.MoveToBackCommand, Source={StaticResource ViewModelMain}}"
                                                                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
+                                Path=(ItemsControl.AlternationIndex)}" />
+                                                                        <MenuItem Header="Merge with above"
+                                                                                  Command="{Binding LayersSubViewModel.MergeWithAboveCommand, Source={StaticResource ViewModelMain}}"
+                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
+                                Path=(ItemsControl.AlternationIndex)}" />
+                                                                        <MenuItem Header="Merge with below"
+                                                                                  Command="{Binding LayersSubViewModel.MergeWithBelowCommand, Source={StaticResource ViewModelMain}}"
+                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
                                 Path=(ItemsControl.AlternationIndex)}" />
                                                                     </ContextMenu>
                                                                 </vws:LayerItem.ContextMenu>

+ 2 - 2
PixiEditorTests/ModelsTests/ImageManipulationTests/BitmapUtilsTests.cs

@@ -45,7 +45,7 @@ namespace PixiEditorTests.ModelsTests.ImageManipulationTests
 
             layers[1].SetPixels(BitmapPixelChanges.FromSingleColoredArray(new[] { cords[1] }, Colors.Red));
 
-            WriteableBitmap outputBitmap = BitmapUtils.CombineLayers(layers, 2, 2);
+            WriteableBitmap outputBitmap = BitmapUtils.CombineLayers(2, 2, layers);
 
             Assert.Equal(Colors.Green, outputBitmap.GetPixel(0, 0));
             Assert.Equal(Colors.Red, outputBitmap.GetPixel(1, 1));
@@ -61,7 +61,7 @@ namespace PixiEditorTests.ModelsTests.ImageManipulationTests
 
             layers[1].SetPixels(BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Red));
 
-            WriteableBitmap outputBitmap = BitmapUtils.CombineLayers(layers, 2, 2);
+            WriteableBitmap outputBitmap = BitmapUtils.CombineLayers(2, 2, layers);
 
             Assert.Equal(Colors.Red, outputBitmap.GetPixel(0, 0));
         }