Browse Source

Merge pull request #19 from PixiEditor/dev

Merge version v0.1 Beta from dev branch
Krzysztof Krysiński 5 years ago
parent
commit
4bd6e3c0ad
93 changed files with 2357 additions and 618 deletions
  1. 0 46
      CODE_OF_CONDUCT.md
  2. 4 6
      CONTRIBUTING.md
  3. 1 1
      LICENSE
  4. 2 2
      PULL_REQUEST_TEMPLATE.md
  5. 27 0
      PixiEditor/Helpers/Converters/DoubleToIntConverter.cs
  6. BIN
      PixiEditor/Images/PixiEditorLogo.png
  7. 16 11
      PixiEditor/Models/Controllers/BitmapManager.cs
  8. 14 5
      PixiEditor/Models/Controllers/BitmapOperationsUtility.cs
  9. 26 10
      PixiEditor/Models/Controllers/ClipboardController.cs
  10. 9 0
      PixiEditor/Models/Controllers/PixelChangesController.cs
  11. 0 7
      PixiEditor/Models/Controllers/ReadonlyToolUtility.cs
  12. 3 3
      PixiEditor/Models/Controllers/UndoManager.cs
  13. 1 1
      PixiEditor/Models/DataHolders/BitmapPixelChanges.cs
  14. 27 2
      PixiEditor/Models/DataHolders/Change.cs
  15. 3 1
      PixiEditor/Models/DataHolders/Document.cs
  16. 2 0
      PixiEditor/Models/DataHolders/NotifyableColor.cs
  17. 2 2
      PixiEditor/Models/DataHolders/Selection.cs
  18. 3 4
      PixiEditor/Models/DataHolders/SerializableDocument.cs
  19. 0 53
      PixiEditor/Models/DataHolders/StackEx.cs
  20. 7 2
      PixiEditor/Models/IO/Exporter.cs
  21. 0 89
      PixiEditor/Models/IO/FilesManager.cs
  22. 17 3
      PixiEditor/Models/IO/Importer.cs
  23. 16 16
      PixiEditor/Models/ImageManipulation/BitmapUtils.cs
  24. 0 11
      PixiEditor/Models/ImageManipulation/Morphology.cs
  25. 48 16
      PixiEditor/Models/Layers/Layer.cs
  26. 0 31
      PixiEditor/Models/Layers/LayerGenerator.cs
  27. 35 0
      PixiEditor/Models/Layers/SerializableLayer.cs
  28. 6 6
      PixiEditor/Models/Position/Coordinates.cs
  29. 8 36
      PixiEditor/Models/Position/CoordinatesCalculator.cs
  30. 1 7
      PixiEditor/Models/Position/MousePositionConverter.cs
  31. 5 0
      PixiEditor/Models/Tools/BitmapOperationTool.cs
  32. 3 0
      PixiEditor/Models/Tools/Tool.cs
  33. 6 1
      PixiEditor/Models/Tools/ToolSettings/Settings/SizeSetting.cs
  34. 3 0
      PixiEditor/Models/Tools/ToolSettings/Toolbars/BasicToolbar.cs
  35. 3 1
      PixiEditor/Models/Tools/ToolSettings/Toolbars/EmptyToolbar.cs
  36. 19 15
      PixiEditor/Models/Tools/ToolSettings/Toolbars/Toolbar.cs
  37. 2 2
      PixiEditor/Models/Tools/Tools/BrightnessTool.cs
  38. 1 1
      PixiEditor/Models/Tools/Tools/CircleTool.cs
  39. 1 1
      PixiEditor/Models/Tools/Tools/FloodFill.cs
  40. 14 3
      PixiEditor/Models/Tools/Tools/LineTool.cs
  41. 4 4
      PixiEditor/Models/Tools/Tools/MoveTool.cs
  42. 1 2
      PixiEditor/Models/Tools/Tools/PenTool.cs
  43. 2 2
      PixiEditor/Models/Tools/Tools/RectangleTool.cs
  44. 6 6
      PixiEditor/Models/Tools/Tools/SelectTool.cs
  45. 3 5
      PixiEditor/PixiEditor.csproj
  46. 3 3
      PixiEditor/Properties/AssemblyInfo.cs
  47. 4 4
      PixiEditor/ViewModels/ViewModelBase.cs
  48. 119 121
      PixiEditor/ViewModels/ViewModelMain.cs
  49. 1 1
      PixiEditor/Views/ImportFilePopup.xaml
  50. 24 5
      PixiEditor/Views/MainWindow.xaml
  51. 1 1
      PixiEditor/Views/NewFilePopup.xaml
  52. 1 1
      PixiEditor/Views/ResizeCanvasPopup.xaml
  53. 1 1
      PixiEditor/Views/ResizeDocumentPopup.xaml
  54. 1 1
      PixiEditor/Views/SaveFilePopup.xaml
  55. 10 10
      PixiEditor/Views/SizePicker.xaml
  56. 6 6
      PixiEditor/Views/SizePicker.xaml.cs
  57. 12 0
      PixiEditorTests/ApplicationCollection.cs
  58. 22 0
      PixiEditorTests/ApplicationFixture.cs
  59. 23 0
      PixiEditorTests/HelpersTests/ConvertersTests/DoubleToIntConverterTest.cs
  60. 3 3
      PixiEditorTests/ModelsTests/ColorsTests/ExtendedColorTests.cs
  61. 28 9
      PixiEditorTests/ModelsTests/ControllersTests/BitmapManagerTests.cs
  62. 50 0
      PixiEditorTests/ModelsTests/ControllersTests/BitmapOperationsUtilityTests.cs
  63. 128 0
      PixiEditorTests/ModelsTests/ControllersTests/ClipboardControllerTests.cs
  64. 80 0
      PixiEditorTests/ModelsTests/ControllersTests/MouseMovementControllerTests.cs
  65. 65 0
      PixiEditorTests/ModelsTests/ControllersTests/PixelChangesControllerTests.cs
  66. 40 0
      PixiEditorTests/ModelsTests/ControllersTests/ReadonlyUtilityTests.cs
  67. 2 1
      PixiEditorTests/ModelsTests/ControllersTests/ShortcutControllerTests.cs
  68. 112 8
      PixiEditorTests/ModelsTests/ControllersTests/UndoManagerTests.cs
  69. 64 0
      PixiEditorTests/ModelsTests/DataHoldersTests/BitmapPixelChangesTests.cs
  70. 150 0
      PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs
  71. 57 0
      PixiEditorTests/ModelsTests/DataHoldersTests/NotifyableColorTests.cs
  72. 64 0
      PixiEditorTests/ModelsTests/DataHoldersTests/SelectionTests.cs
  73. 85 0
      PixiEditorTests/ModelsTests/DataHoldersTests/SerializableDocumentTests.cs
  74. 48 0
      PixiEditorTests/ModelsTests/IO/BinarySerializationTests.cs
  75. 47 0
      PixiEditorTests/ModelsTests/IO/ExporterTests.cs
  76. 60 0
      PixiEditorTests/ModelsTests/IO/ImporterTests.cs
  77. BIN
      PixiEditorTests/ModelsTests/IO/TestImage.png
  78. 98 0
      PixiEditorTests/ModelsTests/ImageManipulationTests/BitmapUtilsTests.cs
  79. 44 0
      PixiEditorTests/ModelsTests/ImageManipulationTests/TransformTests.cs
  80. 144 0
      PixiEditorTests/ModelsTests/LayersTests/LayerTests.cs
  81. 2 2
      PixiEditorTests/ModelsTests/PositionTests/CoordinatesCalculatorTests.cs
  82. 30 0
      PixiEditorTests/ModelsTests/PositionTests/CoordinatesTests.cs
  83. 36 0
      PixiEditorTests/ModelsTests/ToolsTests/BrightnessToolTests.cs
  84. 35 0
      PixiEditorTests/ModelsTests/ToolsTests/LineToolTests.cs
  85. 34 0
      PixiEditorTests/ModelsTests/ToolsTests/RectangleToolTests.cs
  86. 44 0
      PixiEditorTests/ModelsTests/ToolsTests/ToolbarTests/ToolbarBaseTests.cs
  87. 2 7
      PixiEditorTests/PixiEditorTests.csproj
  88. 171 0
      PixiEditorTests/ViewModelsTests/ViewModelMainTests.cs
  89. 54 19
      README.md
  90. BIN
      Screenshot.png
  91. 1 1
      azure-pipelines.yml
  92. BIN
      icon.ico
  93. BIN
      screenshot.png

+ 0 - 46
CODE_OF_CONDUCT.md

@@ -1,46 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a professional setting
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [email protected]. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
-
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/

+ 4 - 6
CONTRIBUTING.md

@@ -5,19 +5,17 @@ Hey! Thanks for being interested in project! It means a lot. But, before contrib
 When contributing to this repository, please first discuss the change you wish to make via issue,
 email, or any other method with the owners of this repository before making a change. 
 
-Please note we have a [code of conduct](https://github.com/flabbet/PixiEditor/blob/master/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
-
 ## Issues
 
 If you want to report a bug, follow steps below, if you want to request a feature, check [this](https://github.com/flabbet/PixiEditor/blob/master/.github/ISSUE_TEMPLATE/feature_request.md)
 
-* First of all, check if the issue is on the [list](https://github.com/flabbet/PixiEditor/issues) and/or [board](https://github.com/flabbet/PixiEditor/projects/1), if yes, upvote it.
+* First of all, check if the issue is on the [list](https://github.com/flabbet/PixiEditor/issues) and/or [board](https://github.com/flabbet/PixiEditor/projects), if yes, upvote it.
 
 * If not, report an issue [here](https://github.com/flabbet/PixiEditor/issues) like that:
  1. Clear as short as possible title
  2. Describe issue as detailed as possible
- 3. Include screenshots, error logs, etc.
- 
+ 3. Include screenshots if possible.
+
  ## Pull Requests
- 
+
  Before pull request, read [this](https://github.com/flabbet/PixiEditor/blob/master/PULL_REQUEST_TEMPLATE.md)

+ 1 - 1
LICENSE

@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2018 flabbet
+Copyright (c) 2018-2020 flabbet
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

+ 2 - 2
PULL_REQUEST_TEMPLATE.md

@@ -1,7 +1,7 @@
  ## Pull Requests
- 
+
  Pull request rules:
- 
+
  1. Clearly describe changes, as detailed as possible
  2. If possible, show examples of usage
  3. Ensure any install or build dependencies are removed before the end of the layer when doing a build.

+ 27 - 0
PixiEditor/Helpers/Converters/DoubleToIntConverter.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using System.Windows.Data;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class DoubleToIntConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value is double || value is float)
+            {
+                double val = (double) value;
+                return (int) val;
+            }
+
+            return value;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

BIN
PixiEditor/Images/PixiEditorLogo.png


+ 16 - 11
PixiEditor/Models/Controllers/BitmapManager.cs

@@ -8,7 +8,7 @@ using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Events;
-using PixiEditor.Models.Images;
+using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
@@ -73,7 +73,7 @@ namespace PixiEditor.Models.Controllers
             MouseController.MousePositionChanged += Controller_MousePositionChanged;
             MouseController.StoppedRecordingChanges += MouseController_StoppedRecordingChanges;
             BitmapOperations = new BitmapOperationsUtility(this);
-            ReadonlyToolUtility = new ReadonlyToolUtility(this);
+            ReadonlyToolUtility = new ReadonlyToolUtility();
         }
 
         public event EventHandler<LayersChangedEventArgs> LayersChanged;
@@ -135,12 +135,7 @@ namespace PixiEditor.Models.Controllers
             if (Mouse.LeftButton == MouseButtonState.Pressed && !IsDraggingViewport()
                                                              && MouseController.ClickedOnCanvas && ActiveDocument != null)
             {
-                if (IsOperationTool(SelectedTool))
-                    BitmapOperations.ExecuteTool(e.NewPosition,
-                        MouseController.LastMouseMoveCoordinates.ToList(), (BitmapOperationTool) SelectedTool);
-                else
-                    ReadonlyToolUtility.ExecuteTool(MouseController.LastMouseMoveCoordinates.ToArray(),
-                        (ReadonlyTool) SelectedTool);
+                ExecuteTool(e.NewPosition);   
             }
             else if (Mouse.LeftButton == MouseButtonState.Released)
             {
@@ -148,9 +143,19 @@ namespace PixiEditor.Models.Controllers
             }
         }
 
+        public void ExecuteTool(Coordinates newPosition)
+        {
+            if (IsOperationTool(SelectedTool))
+                BitmapOperations.ExecuteTool(newPosition,
+                    MouseController.LastMouseMoveCoordinates.ToList(), (BitmapOperationTool)SelectedTool);
+            else
+                ReadonlyToolUtility.ExecuteTool(MouseController.LastMouseMoveCoordinates.ToArray(),
+                    (ReadonlyTool)SelectedTool);
+        }
+
         private bool IsDraggingViewport()
         {
-            return Keyboard.IsKeyDown(Key.LeftShift);
+            return Keyboard.IsKeyDown(Key.LeftShift) && !(SelectedTool is ShapeTool);
         }
 
         private void MouseController_StartedRecordingChanges(object sender, EventArgs e)
@@ -192,7 +197,7 @@ namespace PixiEditor.Models.Controllers
             else
             {
                 GeneratePreviewLayer();
-                PreviewLayer.ApplyPixels(
+                PreviewLayer.SetPixels(
                     BitmapPixelChanges.FromSingleColoredArray(highlightArea, Color.FromArgb(77, 0, 0, 0)));
             }
         }
@@ -212,7 +217,7 @@ namespace PixiEditor.Models.Controllers
 
         public WriteableBitmap GetCombinedLayersBitmap()
         {
-            return BitmapUtils.CombineLayers(ActiveDocument.Layers.ToArray());
+            return BitmapUtils.CombineLayers(ActiveDocument.Layers.ToArray(), ActiveDocument.Width, ActiveDocument.Height);
         }
 
         /// <summary>

+ 14 - 5
PixiEditor/Models/Controllers/BitmapOperationsUtility.cs

@@ -5,7 +5,7 @@ using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.Images;
+using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
@@ -37,12 +37,18 @@ namespace PixiEditor.Models.Controllers
                 old[i] = new LayerChange(
                     BitmapPixelChanges.FromArrays(pixels, oldValues[layers[i]]), i);
                 newChange[i] = new LayerChange(changes, i);
-                layers[i].ApplyPixels(changes);
+                layers[i].SetPixels(changes);
             }
 
             UndoManager.AddUndoChange(new Change("UndoChanges", old, newChange, "Deleted pixels"));
         }
 
+        /// <summary>
+        ///     Executes tool Use() method with given parameters. NOTE: mouseMove is reversed inside function!
+        /// </summary>
+        /// <param name="newPos">Most recent coordinates</param>
+        /// <param name="mouseMove">Last mouse movement coordinates</param>
+        /// <param name="tool">Tool to execute</param>
         public void ExecuteTool(Coordinates newPos, List<Coordinates> mouseMove, BitmapOperationTool tool)
         {
             if (Manager.ActiveDocument != null && tool != null && tool.ToolType != ToolType.None)
@@ -55,6 +61,9 @@ namespace PixiEditor.Models.Controllers
             }
         }
 
+        /// <summary>
+        ///     Applies pixels from preview layer to selected layer
+        /// </summary>
         public void StopAction()
         {
             if (_lastModifiedLayers == null) return;
@@ -66,7 +75,7 @@ namespace PixiEditor.Models.Controllers
 
                BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(_lastModifiedLayers[i].PixelChanges,
                     oldValues, _lastModifiedLayers[i].LayerIndex));
-                Manager.PreviewLayer.Clear();
+               Manager.PreviewLayer = null;
             }
         }
 
@@ -102,7 +111,7 @@ namespace PixiEditor.Models.Controllers
                 GetOldPixelsValues(change.PixelChanges.ChangedPixels.Keys.ToArray()),
                 change.LayerIndex);
 
-            layer.ApplyPixels(change.PixelChanges, false);
+            layer.SetPixels(change.PixelChanges, false);
             return oldPixelsValues;
         }
 
@@ -151,7 +160,7 @@ namespace PixiEditor.Models.Controllers
                 modifiedLayers = ((BitmapOperationTool) Manager.SelectedTool).Use(Manager.ActiveDocument.ActiveLayer,
                     mouseMove.ToArray(), Manager.PrimaryColor);
                 BitmapPixelChanges[] changes = modifiedLayers.Select(x => x.PixelChanges).ToArray();
-                Manager.PreviewLayer.ApplyPixels(BitmapPixelChanges.CombineOverride(changes));
+                Manager.PreviewLayer.SetPixels(BitmapPixelChanges.CombineOverride(changes));
                 _lastModifiedLayers = modifiedLayers;
             }
         }

+ 26 - 10
PixiEditor/Models/Controllers/ClipboardController.cs

@@ -3,24 +3,26 @@ using System.Linq;
 using System.Windows;
 using System.Windows.Media.Imaging;
 using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.Images;
+using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Controllers
 {
-    public class ClipboardController
+    public static class ClipboardController
     {
         /// <summary>
         ///     Copies selection to clipboard in PNG, Bitmap and DIB formats.
         /// </summary>
         /// <param name="layers">Layers where selection is</param>
         /// <param name="selection"></param>
-        public void CopyToClipboard(Layer[] layers, Coordinates[] selection)
+        /// <param name="originalImageWidth">Output </param>
+        /// <param name="originalImageHeight"></param>
+        public static void CopyToClipboard(Layer[] layers, Coordinates[] selection, int originalImageWidth, int originalImageHeight)
         {
             Clipboard.Clear();
-            WriteableBitmap combinedBitmaps = BitmapUtils.CombineLayers(layers);
+            WriteableBitmap combinedBitmaps = BitmapUtils.CombineLayers(layers, originalImageWidth, originalImageHeight);
             using (var pngStream = new MemoryStream())
             {
                 DataObject data = new DataObject();
@@ -37,9 +39,22 @@ namespace PixiEditor.Models.Controllers
             }
         }
 
-        public void PasteFromClipboard()
+        /// <summary>
+        ///     Pastes image from clipboard into new layer.
+        /// </summary>
+        public static void PasteFromClipboard()
         {
-            DataObject dao = (DataObject) Clipboard.GetDataObject();
+            WriteableBitmap image = GetImageFromClipboard();
+            if (image != null) AddImageToLayers(image);
+        }
+
+        /// <summary>
+        ///     Gets image from clipboard, supported PNG, Dib and Bitmap
+        /// </summary>
+        /// <returns>WriteableBitmap</returns>
+        public static WriteableBitmap GetImageFromClipboard()
+        {
+            DataObject dao = (DataObject)Clipboard.GetDataObject();
             WriteableBitmap finalImage = null;
             if (dao.GetDataPresent("PNG"))
                 using (MemoryStream pngStream = dao.GetData("PNG") as MemoryStream)
@@ -56,22 +71,23 @@ namespace PixiEditor.Models.Controllers
             else if (dao.GetDataPresent(DataFormats.Bitmap))
                 finalImage = new WriteableBitmap((dao.GetData(DataFormats.Bitmap) as BitmapSource)!);
 
-            if (finalImage != null) AddImageToLayers(finalImage);
+            return finalImage;
         }
 
-        public bool IsImageInClipboard()
+        public static bool IsImageInClipboard()
         {
             DataObject dao = (DataObject) Clipboard.GetDataObject();
+            if (dao == null) return false;
             return dao.GetDataPresent("PNG") || dao.GetDataPresent(DataFormats.Dib) ||
                    dao.GetDataPresent(DataFormats.Bitmap);
         }
 
-        private void AddImageToLayers(WriteableBitmap image)
+        private static void AddImageToLayers(WriteableBitmap image)
         {
             ViewModelMain.Current.BitmapManager.AddNewLayer("Image", image);
         }
 
-        public BitmapSource BitmapSelectionToBmpSource(WriteableBitmap bitmap, Coordinates[] selection)
+        public static BitmapSource BitmapSelectionToBmpSource(WriteableBitmap bitmap, Coordinates[] selection)
         {
             int offsetX = selection.Min(x => x.X);
             int offsetY = selection.Min(x => x.Y);

+ 9 - 0
PixiEditor/Models/Controllers/PixelChangesController.cs

@@ -13,6 +13,11 @@ namespace PixiEditor.Models.Controllers
         private Dictionary<int, LayerChange> LastChanges { get; set; }
         private Dictionary<int, LayerChange> LastOldValues { get; set; }
 
+        /// <summary>
+        ///     Adds layer changes to controller
+        /// </summary>
+        /// <param name="changes">New changes</param>
+        /// <param name="oldValues">Old values of changes</param>
         public void AddChanges(LayerChange changes, LayerChange oldValues)
         {
             if (changes.PixelChanges.ChangedPixels.Count > 0)
@@ -54,6 +59,10 @@ namespace PixiEditor.Models.Controllers
                     LastOldValues[layerChange.LayerIndex].PixelChanges.ChangedPixels.Add(change.Key, change.Value);
         }
 
+        /// <summary>
+        ///     Returns all changes and deletes them from controller.
+        /// </summary>
+        /// <returns>Tuple array with new changes and old values</returns>
         public Tuple<LayerChange, LayerChange>[] PopChanges()
         {
             //Maybe replace Tuple with custom data type

+ 0 - 7
PixiEditor/Models/Controllers/ReadonlyToolUtility.cs

@@ -5,13 +5,6 @@ namespace PixiEditor.Models.Controllers
 {
     public class ReadonlyToolUtility
     {
-        public BitmapManager Manager { get; set; }
-
-        public ReadonlyToolUtility(BitmapManager manager)
-        {
-            Manager = manager;
-        }
-
         public void ExecuteTool(Coordinates[] mouseMove, ReadonlyTool tool)
         {
             tool.Use(mouseMove);

+ 3 - 3
PixiEditor/Models/Controllers/UndoManager.cs

@@ -8,8 +8,8 @@ namespace PixiEditor.Models.Controllers
     public static class UndoManager
     {
         private static bool _lastChangeWasUndo;
-        public static StackEx<Change> UndoStack { get; set; } = new StackEx<Change>();
-        public static StackEx<Change> RedoStack { get; set; } = new StackEx<Change>();
+        public static Stack<Change> UndoStack { get; set; } = new Stack<Change>();
+        public static Stack<Change> RedoStack { get; set; } = new Stack<Change>();
 
         public static bool CanUndo => UndoStack.Count > 0;
 
@@ -61,7 +61,7 @@ namespace PixiEditor.Models.Controllers
         {
             _lastChangeWasUndo = true;
             Change change = RedoStack.Pop();
-            if (change.ReverseProcess == null)
+            if (change.Process == null)
                 SetPropertyValue(change.Root, change.Property, change.NewValue);
             else
                 change.Process(change.ProcessArguments);

+ 1 - 1
PixiEditor/Models/DataHolders/BitmapPixelChanges.cs

@@ -6,7 +6,7 @@ using PixiEditor.Exceptions;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Position;
 
-namespace PixiEditor.Models.Tools
+namespace PixiEditor.Models.DataHolders
 {
     public struct BitmapPixelChanges
     {

+ 27 - 2
PixiEditor/Models/DataHolders/Change.cs

@@ -19,8 +19,16 @@ namespace PixiEditor.Models.DataHolders
         public object[] ReverseProcessArguments;
         public object Root { get; set; }
 
-
-        public Change(string property, object oldValue, object newValue, string description = "", object root = null)
+        /// <summary>
+        ///     Creates new change for property based undo system
+        /// </summary>
+        /// <param name="property">Name of property</param>
+        /// <param name="oldValue">Old value of property</param>
+        /// <param name="newValue">New value of property</param>
+        /// <param name="description">Description of change</param>
+        /// <param name="root">Custom root for finding property</param>
+        public Change(string property, object oldValue, object newValue,
+            string description = "", object root = null)
         {
             Property = property;
             OldValue = oldValue;
@@ -29,6 +37,15 @@ namespace PixiEditor.Models.DataHolders
             Root = root;
         }
 
+        /// <summary>
+        ///     Creates new change for mixed reverse process based system with new value property based system
+        /// </summary>
+        /// <param name="property">Name of property, which new value will be applied to</param>
+        /// <param name="reverseProcess">Method with reversing value process</param>
+        /// <param name="reverseArguments">Arguments for reverse method</param>
+        /// <param name="newValue">New value of property</param>
+        /// <param name="description">Description of change</param>
+        /// <param name="root">Custom root for finding property</param>
         public Change(string property, Action<object[]> reverseProcess, object[] reverseArguments,
             object newValue, string description = "", object root = null)
         {
@@ -40,6 +57,14 @@ namespace PixiEditor.Models.DataHolders
             Root = root;
         }
 
+        /// <summary>
+        ///     Creates new change for reverse process based system
+        /// </summary>
+        /// <param name="reverseProcess">Method with reversing value process</param>
+        /// <param name="reverseArguments">Arguments for reverse method</param>
+        /// <param name="process">Method with reversing the reversed value</param>
+        /// <param name="processArguments">Arguments for process method</param>
+        /// <param name="description">Description of change</param>
         public Change(Action<object[]> reverseProcess, object[] reverseArguments,
             Action<object[]> process, object[] processArguments, string description = "")
         {

+ 3 - 1
PixiEditor/Models/DataHolders/Document.cs

@@ -173,6 +173,8 @@ namespace PixiEditor.Models.DataHolders
             for (int i = 0; i < Layers.Count; i++)
             {
                 Layers[i].Offset = offset[i];
+                Layers[i].MaxWidth = newWidth;
+                Layers[i].MaxHeight = newHeight;
             }
 
             Width = newWidth;
@@ -276,7 +278,7 @@ namespace PixiEditor.Models.DataHolders
 
             var contentCenter = CoordinatesCalculator.GetCenterPoint(points.Coords1, points.Coords2);
             var documentCenter = CoordinatesCalculator.GetCenterPoint(new Coordinates(0, 0),
-                new Coordinates(Width - 1, Height - 1));
+                new Coordinates(Width, Height));
             Coordinates moveVector = new Coordinates(documentCenter.X - contentCenter.X, documentCenter.Y - contentCenter.Y);
 
             MoveOffsets(moveVector);

+ 2 - 0
PixiEditor/Models/DataHolders/NotifyableColor.cs

@@ -67,6 +67,8 @@ namespace PixiEditor.Models.DataHolders
             B = color.B;
         }
 
+        public NotifyableColor(){}
+
         public event EventHandler ColorChanged;
 
         public void SetArgb(byte a, byte r, byte g, byte b)

+ 2 - 2
PixiEditor/Models/DataHolders/Selection.cs

@@ -52,12 +52,12 @@ namespace PixiEditor.Models.DataHolders
                     break;
             }
 
-            SelectionLayer.ApplyPixels(BitmapPixelChanges.FromSingleColoredArray(selection, selectionColor));
+            SelectionLayer.SetPixels(BitmapPixelChanges.FromSingleColoredArray(selection, selectionColor));
         }
 
         public void Clear()
         {
-            SelectionLayer.Clear();
+            SelectionLayer = new Layer("_selectionLayer");
             SelectedPoints.Clear();
         }
     }

+ 3 - 4
PixiEditor/Models/DataHolders/SerializableDocument.cs

@@ -3,7 +3,7 @@ using System.Collections.ObjectModel;
 using System.Linq;
 using System.Windows;
 using System.Windows.Media;
-using PixiEditor.Models.Images;
+using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
 
 namespace PixiEditor.Models.DataHolders
@@ -35,17 +35,16 @@ namespace PixiEditor.Models.DataHolders
             return document;
         }
 
-        private ObservableCollection<Layer> ToLayers()
+        public ObservableCollection<Layer> ToLayers()
         {
             ObservableCollection<Layer> layers = new ObservableCollection<Layer>();
             for (int i = 0; i < Layers.Length; i++)
             {
                 SerializableLayer serLayer = Layers[i];
                 Layer layer =
-                    new Layer(BitmapUtils.BytesToWriteableBitmap(serLayer.Width, serLayer.Height, serLayer.BitmapBytes))
+                    new Layer(serLayer.Name,BitmapUtils.BytesToWriteableBitmap(serLayer.Width, serLayer.Height, serLayer.BitmapBytes))
                     {
                         IsVisible = serLayer.IsVisible,
-                        Name = serLayer.Name,
                         Offset = new Thickness(serLayer.OffsetX, serLayer.OffsetY, 0, 0),
                         Opacity = serLayer.Opacity
                     };

+ 0 - 53
PixiEditor/Models/DataHolders/StackEx.cs

@@ -1,53 +0,0 @@
-using System.Collections.Generic;
-
-namespace PixiEditor.Models.DataHolders
-{
-    public class StackEx<T>
-    {
-        public int Count => items.Count;
-
-        public T First => items[0];
-        private readonly List<T> items = new List<T>();
-
-        public void Clear()
-        {
-            items.Clear();
-        }
-
-        /// <summary>
-        ///     Returns top object without deleting it.
-        /// </summary>
-        /// <returns>Returns n - 1 item from stack.</returns>
-        public T Peek()
-        {
-            return items[items.Count - 1];
-        }
-
-        public void Push(T item)
-        {
-            items.Add(item);
-        }
-
-        public T Pop()
-        {
-            if (items.Count > 0)
-            {
-                T temp = items[items.Count - 1];
-                items.RemoveAt(items.Count - 1);
-                return temp;
-            }
-
-            return default;
-        }
-
-        public void PushToBottom(T item)
-        {
-            items.Insert(0, item);
-        }
-
-        public void Remove(int itemAtPosition)
-        {
-            items.RemoveAt(itemAtPosition);
-        }
-    }
-}

+ 7 - 2
PixiEditor/Models/IO/Exporter.cs

@@ -13,7 +13,12 @@ namespace PixiEditor.Models.IO
         public static Size FileDimensions;
         public static string SaveDocumentPath { get; set; }
 
-        public static void SaveAsNewEditableFile(Document document, bool updateWorkspacePath = false)
+        /// <summary>
+        ///     Saves document as .pixi file that contains all document data
+        /// </summary>
+        /// <param name="document">Document to save</param>
+        /// <param name="updateWorkspacePath">Should editor remember dialog path for further saves</param>
+        public static void SaveAsEditableFileWithDialog(Document document, bool updateWorkspacePath = false)
         {
             SaveFileDialog dialog = new SaveFileDialog
             {
@@ -62,7 +67,7 @@ namespace PixiEditor.Models.IO
         /// <param name="exportWidth">File width</param>
         /// <param name="exportHeight">File height</param>
         /// <param name="bitmap">Bitmap to save</param>
-        private static void SaveAsPng(string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
+        public static void SaveAsPng(string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
         {
             try
             {

+ 0 - 89
PixiEditor/Models/IO/FilesManager.cs

@@ -1,89 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using Newtonsoft.Json;
-
-namespace PixiEditor.Models.IO
-{
-    public static class FilesManager
-    {
-        public static string TempFolderPath =>
-            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"PixiEditor\Temp");
-
-        public static string RedoStackPath => Path.Combine(TempFolderPath, @"RedoStack");
-
-        public static string UndoStackPath => Path.Combine(TempFolderPath, @"UndoStack");
-
-        /// <summary>
-        ///     Saves object to file on disk using binary formatter
-        /// </summary>
-        /// <param name="obj">Object to be saved</param>
-        public static void SaveObjectToJsonFile<T>(T obj, string fileName) where T : new()
-        {
-            try
-            {
-                SaveSerializedObjectToFile(obj, fileName);
-            }
-            catch (IOException ex)
-            {
-                Debug.WriteLine(ex.Message);
-            }
-        }
-
-
-        public static void RemoveFile(string path)
-        {
-            File.Delete(path);
-        }
-
-        /// <summary>
-        ///     Removes all files from directory
-        /// </summary>
-        /// <param name="path"></param>
-        public static void ClearDirectory(string path)
-        {
-            string[] filesInDirectory = Directory.GetFiles(path);
-            for (int i = 0; i < filesInDirectory.Length; i++) File.Delete(filesInDirectory[i]);
-        }
-
-        private static void SaveSerializedObjectToFile(object obj, string filename)
-        {
-            using (TextWriter writer = new StreamWriter(filename, false))
-            {
-                var contentsToWriteToFile = JsonConvert.SerializeObject(obj);
-                writer.Write(contentsToWriteToFile);
-            }
-        }
-
-        public static T ReadObjectFromFile<T>(string filePath) where T : new()
-        {
-            using (TextReader reader = new StreamReader(filePath))
-            {
-                var fileContent = reader.ReadToEnd();
-                return JsonConvert.DeserializeObject<T>(fileContent);
-            }
-        }
-
-        /// <summary>
-        ///     Creates and cleares temp directories
-        /// </summary>
-        public static void InitializeTempDirectories()
-        {
-            CreateTempDirectories();
-            ClearTempDirectoriesContent();
-        }
-
-        private static void CreateTempDirectories()
-        {
-            Directory.CreateDirectory(TempFolderPath);
-            Directory.CreateDirectory(Path.Combine(TempFolderPath, "UndoStack"));
-            Directory.CreateDirectory(Path.Combine(TempFolderPath, "RedoStack"));
-        }
-
-        public static void ClearTempDirectoriesContent()
-        {
-            ClearDirectory(RedoStackPath);
-            ClearDirectory(UndoStackPath);
-        }
-    }
-}

+ 17 - 3
PixiEditor/Models/IO/Importer.cs

@@ -15,6 +15,21 @@ namespace PixiEditor.Models.IO
         /// <param name="height">New height of image.</param>
         /// <returns></returns>
         public static WriteableBitmap ImportImage(string path, int width, int height)
+        {
+            var wbmp = ImportImage(path);
+            if (wbmp.PixelWidth != width || wbmp.PixelHeight != height)
+            {
+                return wbmp.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+            }
+
+            return wbmp;
+        }
+
+        /// <summary>
+        ///     Imports image from path and resizes it to given dimensions
+        /// </summary>
+        /// <param name="path">Path of image.</param>
+        public static WriteableBitmap ImportImage(string path)
         {
             Uri uri = new Uri(path);
             BitmapImage bitmap = new BitmapImage();
@@ -22,9 +37,7 @@ namespace PixiEditor.Models.IO
             bitmap.UriSource = uri;
             bitmap.EndInit();
 
-            var wbmp = new WriteableBitmap(bitmap);
-            wbmp = wbmp.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
-            return wbmp;
+            return BitmapFactory.ConvertToPbgra32Format(bitmap);
         }
 
         public static Document ImportDocument(string path)
@@ -34,6 +47,7 @@ namespace PixiEditor.Models.IO
 
         public static bool IsSupportedFile(string path)
         {
+            path = path.ToLower();
             return path.EndsWith(".pixi") || path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg");
         }
     }

+ 16 - 16
PixiEditor/Models/Images/BitmapUtils.cs → PixiEditor/Models/ImageManipulation/BitmapUtils.cs

@@ -1,19 +1,24 @@
 using System;
 using System.Collections.Generic;
 using System.Drawing;
-using System.Linq;
 using System.Windows;
 using System.Windows.Interop;
 using System.Windows.Media.Imaging;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
-using PixiEditor.ViewModels;
 using Color = System.Windows.Media.Color;
 
-namespace PixiEditor.Models.Images
+namespace PixiEditor.Models.ImageManipulation
 {
     public static class BitmapUtils
     {
+        /// <summary>
+        ///     Converts pixel bytes to WriteableBitmap
+        /// </summary>
+        /// <param name="currentBitmapWidth">Width of bitmap</param>
+        /// <param name="currentBitmapHeight">Height of bitmap</param>
+        /// <param name="byteArray">Bitmap byte array</param>
+        /// <returns>WriteableBitmap</returns>
         public static WriteableBitmap BytesToWriteableBitmap(int currentBitmapWidth, int currentBitmapHeight,
             byte[] byteArray)
         {
@@ -22,20 +27,15 @@ namespace PixiEditor.Models.Images
             return bitmap;
         }
 
-        public static BitmapSource BitmapToBitmapSource(Bitmap bitmap)
+        /// <summary>
+        ///     Converts layers bitmaps into one bitmap.
+        /// </summary>
+        /// <param name="layers">Layers to combine</param>
+        /// <param name="width">Width 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)
         {
-            return Imaging.CreateBitmapSourceFromHBitmap(
-                bitmap.GetHbitmap(),
-                IntPtr.Zero,
-                Int32Rect.Empty,
-                BitmapSizeOptions.FromEmptyOptions());
-        }
-
-        public static WriteableBitmap CombineLayers(Layer[] layers)
-        {
-            int width = ViewModelMain.Current.BitmapManager.ActiveDocument.Width;
-            int height = ViewModelMain.Current.BitmapManager.ActiveDocument.Height;
-
             WriteableBitmap finalBitmap = BitmapFactory.New(width, height);
 
             using (finalBitmap.GetBitmapContext())

+ 0 - 11
PixiEditor/Models/ImageManipulation/Morphology.cs

@@ -58,7 +58,6 @@ namespace PixiEditor.Models.ImageManipulation
             int minX = points.Min(x => x.X);
             int minY = points.Min(x => x.Y);
             byte[,] array = new byte[dimensions.Item1 + margin * 2, dimensions.Item2 + margin * 2];
-            //Debug.Write("----------\n");
 
             for (int y = 0; y < dimensions.Item2 + margin; y++)
             for (int x = 0; x < dimensions.Item1 + margin; x++)
@@ -66,16 +65,6 @@ namespace PixiEditor.Models.ImageManipulation
                 Coordinates cords = new Coordinates(x + minX, y + minY);
                 array[x + margin, y + margin] = points.Contains(cords) ? (byte) 1 : (byte) 0;
             }
-
-            //for (int y = 0; y < array.GetLength(1); y++)
-            //{
-            //    for (int x = 0; x < array.GetLength(0); x++)
-            //    {
-            //        Debug.Write($"{array[x, y]} ");
-            //    }
-            //    Debug.Write("\n");
-            //}
-
             return array;
         }
 

+ 48 - 16
PixiEditor/Models/Layers/Layer.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Windows;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 
@@ -110,8 +111,7 @@ namespace PixiEditor.Models.Layers
         public Layer(string name)
         {
             Name = name;
-            Layer layer = LayerGenerator.Generate(0, 0);
-            LayerBitmap = layer.LayerBitmap;
+            LayerBitmap = BitmapFactory.New(0, 0);
             Width = 0;
             Height = 0;
         }
@@ -119,18 +119,18 @@ namespace PixiEditor.Models.Layers
         public Layer(string name, int width, int height)
         {
             Name = name;
-            Layer layer = LayerGenerator.Generate(width, height);
-            LayerBitmap = layer.LayerBitmap;
+            LayerBitmap = BitmapFactory.New(width, height);
             Width = width;
             Height = height;
         }
 
 
-        public Layer(WriteableBitmap layerBitmap)
+        public Layer(string name, WriteableBitmap layerBitmap)
         {
+            Name = name;
             LayerBitmap = layerBitmap;
-            Width = (int) layerBitmap.Width;
-            Height = (int) layerBitmap.Height;
+            Width = layerBitmap.PixelWidth;
+            Height = layerBitmap.PixelHeight;
         }
 
         /// <summary>
@@ -139,17 +139,25 @@ namespace PixiEditor.Models.Layers
         /// <returns></returns>
         public Layer Clone()
         {
-            return new Layer(LayerBitmap.Clone())
+            return new Layer(Name, LayerBitmap.Clone())
             {
-                _isVisible = this._isVisible,
-                Name = this.Name,
+                IsVisible = this.IsVisible,
                 Offset = this.Offset,
                 MaxHeight = this.MaxHeight,
                 MaxWidth = this.MaxWidth,
-                Opacity = this.Opacity
+                Opacity = this.Opacity,
+                IsActive = this.IsActive,
+                IsRenaming = this.IsRenaming
             };
         }
 
+        /// <summary>
+        ///     Resizes bitmap with it's content using NearestNeighbor interpolation
+        /// </summary>
+        /// <param name="width">New width</param>
+        /// <param name="height">New height</param>
+        /// <param name="newMaxWidth">New layer maximum width, this should be document width</param>
+        /// <param name="newMaxHeight">New layer maximum height, this should be document height</param>
         public void Resize(int width, int height, int newMaxWidth, int newMaxHeight)
         {
             LayerBitmap = LayerBitmap.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
@@ -170,7 +178,7 @@ namespace PixiEditor.Models.Layers
         }
 
         /// <summary>
-        ///     Returns pixel color by x and y coordinates relative to document using (x - OffsetX) formula.
+        ///     Returns pixel color of x and y coordinates relative to document using (x - OffsetX) formula.
         /// </summary>
         /// <param name="x">Viewport relative X</param>
         /// <param name="y">Viewport relative Y</param>
@@ -204,9 +212,9 @@ namespace PixiEditor.Models.Layers
         /// <param name="color">Color of pixel</param>
         /// <param name="dynamicResize">Resizes bitmap to fit content</param>
         /// <param name="applyOffset">Converts pixels coordinates to relative to bitmap</param>
-        public void ApplyPixel(Coordinates coordinates, Color color, bool dynamicResize = true, bool applyOffset = true)
+        public void SetPixel(Coordinates coordinates, Color color, bool dynamicResize = true, bool applyOffset = true)
         {
-            ApplyPixels(BitmapPixelChanges.FromSingleColoredArray(new []{ coordinates }, color), dynamicResize, applyOffset);
+            SetPixels(BitmapPixelChanges.FromSingleColoredArray(new []{ coordinates }, color), dynamicResize, applyOffset);
         }
 
         /// <summary>
@@ -215,7 +223,7 @@ namespace PixiEditor.Models.Layers
         /// <param name="pixels">Pixels to apply</param>
         /// <param name="dynamicResize">Resizes bitmap to fit content</param>
         /// <param name="applyOffset">Converts pixels coordinates to relative to bitmap</param>
-        public void ApplyPixels(BitmapPixelChanges pixels, bool dynamicResize = true, bool applyOffset = true)
+        public void SetPixels(BitmapPixelChanges pixels, bool dynamicResize = true, bool applyOffset = true)
         {
             if (pixels.ChangedPixels == null || pixels.ChangedPixels.Count == 0) return;
             if(dynamicResize)
@@ -241,6 +249,11 @@ namespace PixiEditor.Models.Layers
                 d => d.Value);
         }
 
+        /// <summary>
+        ///     Converts absolute coordinates array to relative to this layer coordinates array.
+        /// </summary>
+        /// <param name="nonRelativeCords">absolute coordinates array</param>
+        /// <returns></returns>
         public Coordinates[] ConvertToRelativeCoordinates(Coordinates[] nonRelativeCords)
         {
             Coordinates[] result = new Coordinates[nonRelativeCords.Length];
@@ -330,6 +343,9 @@ namespace PixiEditor.Models.Layers
             }
         }
 
+        /// <summary>
+        ///     Changes size of bitmap to fit content
+        /// </summary>
         public void ClipCanvas()
         {
             var points = GetEdgePoints();
@@ -391,11 +407,18 @@ namespace PixiEditor.Models.Layers
             }
         }
 
+        /// <summary>
+        ///     Clears bitmap
+        /// </summary>
         public void Clear()
         {
             LayerBitmap.Clear();
         }
 
+        /// <summary>
+        ///     Converts layer WriteableBitmap to byte array
+        /// </summary>
+        /// <returns></returns>
         public byte[] ConvertBitmapToBytes()
         {
             LayerBitmap.Lock();
@@ -404,7 +427,16 @@ namespace PixiEditor.Models.Layers
             return byteArray;
         }
 
-        public void ResizeCanvas(int offsetX, int offsetY, int offsetXSrc, int offsetYSrc, int newWidth, int newHeight)
+        /// <summary>
+        ///     Resizes canvas to new size with specified offset.
+        /// </summary>
+        /// <param name="offsetX"></param>
+        /// <param name="offsetY"></param>
+        /// <param name="offsetXSrc"></param>
+        /// <param name="offsetYSrc"></param>
+        /// <param name="newWidth"></param>
+        /// <param name="newHeight"></param>
+        private void ResizeCanvas(int offsetX, int offsetY, int offsetXSrc, int offsetYSrc, int newWidth, int newHeight)
         {
             int iteratorHeight = Height > newHeight ? newHeight : Height;
             int count = Width > newWidth ? newWidth : Width;

+ 0 - 31
PixiEditor/Models/Layers/LayerGenerator.cs

@@ -1,31 +0,0 @@
-using System.Windows.Media.Imaging;
-
-namespace PixiEditor.Models.Layers
-{
-    public static class LayerGenerator
-    {
-        /// <summary>
-        ///     Generating useable layer with image and bitmap
-        /// </summary>
-        /// <param name="imageWidth">Width of layer.</param>
-        /// <param name="imageHeight">Height of layer.</param>
-        /// <returns></returns>
-        public static Layer Generate(int imageWidth, int imageHeight)
-        {
-            return new Layer(GenerateBitmap(imageWidth, imageHeight));
-        }
-
-        /// <summary>
-        ///     Generates bitmap ready to work with
-        /// </summary>
-        /// <param name="bitmapWidth">Width of bitmap.</param>
-        /// <param name="imageHeight">Height of bitmap.</param>
-        /// <returns></returns>
-        private static WriteableBitmap GenerateBitmap(int bitmapWidth, int imageHeight)
-        {
-            WriteableBitmap bitmap = BitmapFactory.New(bitmapWidth, imageHeight);
-            bitmap.Clear(System.Windows.Media.Colors.Transparent);
-            return bitmap;
-        }
-    }
-}

+ 35 - 0
PixiEditor/Models/Layers/SerializableLayer.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 
 namespace PixiEditor.Models.Layers
 {
@@ -8,6 +9,8 @@ namespace PixiEditor.Models.Layers
         public string Name { get; set; }
         public int Width { get; set; }
         public int Height { get; set; }
+        public int MaxWidth { get; set; }
+        public int MaxHeight { get; set; }
         public byte[] BitmapBytes { get; set; }
         public bool IsVisible { get; set; }
         public int OffsetX { get; set; }
@@ -24,6 +27,38 @@ namespace PixiEditor.Models.Layers
             OffsetX = (int)layer.Offset.Left;
             OffsetY = (int)layer.Offset.Top;
             Opacity = layer.Opacity;
+            MaxWidth = layer.MaxWidth;
+            MaxHeight = layer.MaxHeight;
+        }
+
+        public override bool Equals(object? obj)
+        {
+            if (obj == null || obj.GetType() != typeof(SerializableLayer)) return false;
+
+            SerializableLayer layer = (SerializableLayer) obj;
+
+            return Equals(layer);
+        }
+
+        protected bool Equals(SerializableLayer other)
+        {
+            return Name == other.Name && Width == other.Width && Height == other.Height && MaxWidth == other.MaxWidth && MaxHeight == other.MaxHeight && BitmapBytes.SequenceEqual(other.BitmapBytes) && IsVisible == other.IsVisible && OffsetX == other.OffsetX && OffsetY == other.OffsetY && Opacity.Equals(other.Opacity);
+        }
+
+        public override int GetHashCode()
+        {
+            var hashCode = new HashCode();
+            hashCode.Add(Name);
+            hashCode.Add(Width);
+            hashCode.Add(Height);
+            hashCode.Add(MaxWidth);
+            hashCode.Add(MaxHeight);
+            hashCode.Add(BitmapBytes);
+            hashCode.Add(IsVisible);
+            hashCode.Add(OffsetX);
+            hashCode.Add(OffsetY);
+            hashCode.Add(Opacity);
+            return hashCode.ToHashCode();
         }
     }
 }

+ 6 - 6
PixiEditor/Models/Position/Coordinates.cs

@@ -14,7 +14,7 @@
 
         public override string ToString()
         {
-            return $"x: {X}, y: {Y}";
+            return $"{X}, {Y}";
         }
 
         public static bool operator ==(Coordinates c1, Coordinates c2)
@@ -37,12 +37,12 @@
         {
             unchecked
             {
-                const int HashingBase = (int) 2166136261;
-                const int HashingMultiplier = 16777619;
+                const int hashingBase = (int) 2166136261;
+                const int hashingMultiplier = 16777619;
 
-                int hash = HashingBase;
-                hash = (hash * HashingMultiplier) ^ (!ReferenceEquals(null, X) ? X.GetHashCode() : 0);
-                hash = (hash * HashingMultiplier) ^ (!ReferenceEquals(null, Y) ? Y.GetHashCode() : 0);
+                int hash = hashingBase;
+                hash = (hash * hashingMultiplier) ^ (!ReferenceEquals(null, X) ? X.GetHashCode() : 0);
+                hash = (hash * hashingMultiplier) ^ (!ReferenceEquals(null, Y) ? Y.GetHashCode() : 0);
                 return hash;
             }
         }

+ 8 - 36
PixiEditor/Models/Position/CoordinatesCalculator.cs

@@ -43,6 +43,14 @@ namespace PixiEditor.Models.Position
             return new Coordinates(x, y);
         }
 
+        /// <summary>
+        ///     Calculates coordinates of rectangle by edge points x1, y1, x2, y2
+        /// </summary>
+        /// <param name="x1">Top left x point</param>
+        /// <param name="y1">Top left y position</param>
+        /// <param name="x2">Bottom right x position</param>
+        /// <param name="y2">Bottom right Y position</param>
+        /// <returns></returns>
         public static Coordinates[] RectangleToCoordinates(int x1, int y1, int x2, int y2)
         {
             x2++;
@@ -136,41 +144,5 @@ namespace PixiEditor.Models.Position
             bitmap.Unlock();
             return -1;
         }
-
-        /// <summary>
-        ///     Finds most top-left pixel on each layer.
-        /// </summary>
-        /// <param name="document"></param>
-        /// <returns>Most top-left pixel in each layer</returns>
-        public static Coordinates[] GetSmallestPixels(Document document)
-        {
-            Coordinates[] smallestPixels = new Coordinates[document.Layers.Count];
-            for (int i = 0; i < smallestPixels.Length; i++)
-            {
-                Coordinates point = FindMinEdgeNonTransparentPixel(document.Layers[i].LayerBitmap);
-                if (point.X >= 0 && point.Y >= 0)
-                    smallestPixels[i] = point;
-            }
-
-            return smallestPixels;
-        }
-
-        /// <summary>
-        ///     Finds most bottom-right pixel on each layer.
-        /// </summary>
-        /// <param name="document"></param>
-        /// <returns>Most bottom-right pixel in each layer</returns>
-        public static Coordinates[] GetBiggestPixels(Document document)
-        {
-            Coordinates[] biggestPixels = new Coordinates[document.Layers.Count];
-            for (int i = 0; i < biggestPixels.Length; i++)
-            {
-                Coordinates point = FindMostEdgeNonTransparentPixel(document.Layers[i].LayerBitmap);
-                if (point.X >= 0 && point.Y >= 0)
-                    biggestPixels[i] = point;
-            }
-
-            return biggestPixels;
-        }
     }
 }

+ 1 - 7
PixiEditor/Models/Position/MousePositionConverter.cs

@@ -1,5 +1,6 @@
 using System.Runtime.InteropServices;
 using System.Windows;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 
 namespace PixiEditor.Models.Position
@@ -8,13 +9,6 @@ namespace PixiEditor.Models.Position
     {
         public static Coordinates CurrentCoordinates { get; set; }
 
-        public static Coordinates MousePositionToCoordinates(Layer baseLayer, Point mousePosition)
-        {
-            int xCoord = (int) (mousePosition.X / baseLayer.Width);
-            int yCoord = (int) (mousePosition.Y / baseLayer.Height);
-            return new Coordinates(xCoord, yCoord);
-        }
-
         [DllImport("user32.dll")]
         private static extern bool GetCursorPos(out System.Drawing.Point point);
 

+ 5 - 0
PixiEditor/Models/Tools/BitmapOperationTool.cs

@@ -15,5 +15,10 @@ namespace PixiEditor.Models.Tools
         {
             return new[] { new LayerChange(changes, layer) };
         }
+
+        protected LayerChange[] Only(BitmapPixelChanges changes, int layerIndex)
+        {
+            return new[] { new LayerChange(changes, layerIndex) };
+        }
     }
 }

+ 3 - 0
PixiEditor/Models/Tools/Tool.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Tools.ToolSettings;
+using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.Models.Tools
 {
@@ -22,7 +23,9 @@ namespace PixiEditor.Models.Tools
         }
 
         public Cursor Cursor { get; set; } = Cursors.Arrow;
+
         public Toolbar Toolbar { get; set; } = new EmptyToolbar();
+
         private bool _isActive;
 
         public virtual void OnMouseDown()

+ 6 - 1
PixiEditor/Models/Tools/ToolSettings/Settings/SizeSetting.cs

@@ -20,12 +20,17 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
         {
             TextBox tb = new TextBox
             {
-                Style = Application.Current.FindResource("DarkTextBoxStyle") as Style,
                 TextAlignment = TextAlignment.Center,
                 MaxLength = 4,
                 Width = 40,
                 Height = 20
             };
+
+            if (Application.Current != null)
+            {
+                tb.Style = (Style)Application.Current.TryFindResource("DarkTextBoxStyle"); ;
+            }
+
             Binding binding = new Binding("Value")
             {
                 Converter = new ToolSizeToIntConverter(),

+ 3 - 0
PixiEditor/Models/Tools/ToolSettings/Toolbars/BasicToolbar.cs

@@ -2,6 +2,9 @@
 
 namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
 {
+    /// <summary>
+    ///     Toolbar with size setting
+    /// </summary>
     public class BasicToolbar : Toolbar
     {
         public BasicToolbar()

+ 3 - 1
PixiEditor/Models/Tools/ToolSettings/Toolbars/EmptyToolbar.cs

@@ -1,4 +1,6 @@
-namespace PixiEditor.Models.Tools.ToolSettings
+using PixiEditor.Models.Tools.ToolSettings.Toolbars;
+
+namespace PixiEditor.Models.Tools.ToolSettings
 {
     public class EmptyToolbar : Toolbar
     {

+ 19 - 15
PixiEditor/Models/Tools/ToolSettings/Toolbars/Toolbar.cs

@@ -1,22 +1,23 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
 
-namespace PixiEditor.Models.Tools.ToolSettings
+namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
 {
     public abstract class Toolbar
     {
-        private static readonly List<Setting> _sharedSettings = new List<Setting>();
+        private static readonly List<Setting> SharedSettings = new List<Setting>();
         public ObservableCollection<Setting> Settings { get; set; } = new ObservableCollection<Setting>();
 
+        /// <summary>
+        ///     Gets setting in toolbar by name.
+        /// </summary>
+        /// <param name="name">Setting name, non case sensitive</param>
+        /// <returns></returns>
         public virtual Setting GetSetting(string name)
         {
-            return Settings.FirstOrDefault(x => x.Name == name);
-        }
-
-        public virtual Setting[] GetSettings(string name)
-        {
-            return Settings.Where(x => x.Name == name).ToArray();
+            return Settings.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.CurrentCultureIgnoreCase));
         }
 
         /// <summary>
@@ -25,17 +26,20 @@ namespace PixiEditor.Models.Tools.ToolSettings
         public void SaveToolbarSettings()
         {
             for (int i = 0; i < Settings.Count; i++)
-                if (_sharedSettings.Any(x => x.Name == Settings[i].Name))
-                    _sharedSettings.First(x => x.Name == Settings[i].Name).Value = Settings[i].Value;
+                if (SharedSettings.Any(x => x.Name == Settings[i].Name))
+                    SharedSettings.First(x => x.Name == Settings[i].Name).Value = Settings[i].Value;
                 else
-                    _sharedSettings.Add(Settings[i]);
+                    SharedSettings.Add(Settings[i]);
         }
 
+        /// <summary>
+        ///     Loads common settings saved from previous tools to current one.
+        /// </summary>
         public void LoadSharedSettings()
         {
-            for (int i = 0; i < _sharedSettings.Count; i++)
-                if (Settings.Any(x => x.Name == _sharedSettings[i].Name))
-                    Settings.First(x => x.Name == _sharedSettings[i].Name).Value = _sharedSettings[i].Value;
+            for (int i = 0; i < SharedSettings.Count; i++)
+                if (Settings.Any(x => x.Name == SharedSettings[i].Name))
+                    Settings.First(x => x.Name == SharedSettings[i].Name).Value = SharedSettings[i].Value;
         }
     }
 }

+ 2 - 2
PixiEditor/Models/Tools/Tools/BrightnessTool.cs

@@ -24,7 +24,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public BrightnessTool()
         {
-            Tooltip = "Makes pixel brighter or darker pixel (U)";
+            Tooltip = "Makes pixel brighter or darker pixel (U). Hold Ctrl to make pixel darker.";
             Toolbar = new BrightnessToolToolbar(CorrectionFactor);
         }
 
@@ -51,7 +51,7 @@ namespace PixiEditor.Models.Tools.Tools
             return layersChanges;
         }
 
-        private BitmapPixelChanges ChangeBrightness(Layer layer, Coordinates coordinates, int toolSize,
+        public BitmapPixelChanges ChangeBrightness(Layer layer, Coordinates coordinates, int toolSize,
             float correctionFactor)
         {
             DoubleCords centeredCoords = CoordinatesCalculator.CalculateThicknessCenter(coordinates, toolSize);

+ 1 - 1
PixiEditor/Models/Tools/Tools/CircleTool.cs

@@ -15,7 +15,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public CircleTool()
         {
-            Tooltip = "Draws circle on cavnas (C)";
+            Tooltip = "Draws circle on canvas (C). Hold Shift to draw even circle.";
         }
 
         public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)

+ 1 - 1
PixiEditor/Models/Tools/Tools/FloodFill.cs

@@ -50,7 +50,7 @@ namespace PixiEditor.Models.Tools.Tools
                     if (clone.GetPixel(relativeCords.X, relativeCords.Y) == colorToReplace)
                     {
                         changedCoords.Add(new Coordinates(cords.X, cords.Y));
-                        clone.ApplyPixel(new Coordinates(cords.X, cords.Y), newColor);
+                        clone.SetPixel(new Coordinates(cords.X, cords.Y), newColor);
                         stack.Push(new Coordinates(cords.X, cords.Y - 1));
                         stack.Push(new Coordinates(cords.X + 1, cords.Y));
                         stack.Push(new Coordinates(cords.X, cords.Y + 1));

+ 14 - 3
PixiEditor/Models/Tools/Tools/LineTool.cs

@@ -17,7 +17,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public LineTool()
         {
-            Tooltip = "Draws line on canvas (L)";
+            Tooltip = "Draws line on canvas (L). Hold Shift to draw even line.";
             Toolbar = new BasicToolbar();
         }
 
@@ -26,11 +26,22 @@ namespace PixiEditor.Models.Tools.Tools
             var pixels =
                 BitmapPixelChanges.FromSingleColoredArray(
                     CreateLine(coordinates, 
-                        (int) Toolbar.GetSetting("ToolSize").Value, CapType.Round, CapType.Round), color);
+                        (int) Toolbar.GetSetting("ToolSize").Value, CapType.Square, CapType.Square), color);
             return Only(pixels, layer);
         }
 
-        public Coordinates[] CreateLine(Coordinates[] coordinates, int thickness, CapType startCap, CapType endCap)
+        public Coordinates[] CreateLine(Coordinates start, Coordinates end, int thickness)
+        {
+            return CreateLine(new[] { end, start }, thickness, CapType.Square, CapType.Square);
+        }
+
+        public Coordinates[] CreateLine(Coordinates start, Coordinates end, int thickness, CapType startCap,
+            CapType endCap)
+        {
+            return CreateLine(new[] {end, start}, thickness, startCap, endCap);
+        }
+
+        private Coordinates[] CreateLine(Coordinates[] coordinates, int thickness, CapType startCap, CapType endCap)
         {
             Coordinates startingCoordinates = coordinates[^1];
             Coordinates latestCoordinates = coordinates[0];

+ 4 - 4
PixiEditor/Models/Tools/Tools/MoveTool.cs

@@ -8,7 +8,7 @@ using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
-using PixiEditor.Models.Images;
+using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.ViewModels;
@@ -33,7 +33,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public MoveTool()
         {
-            Tooltip = "Moves selected pixels. (V)";
+            Tooltip = "Moves selected pixels (V). Hold Ctrl to move all layers";
             Cursor = Cursors.Arrow;
             HideHighlight = true;
             RequiresPreviewLayer = true;
@@ -64,7 +64,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public override void OnMouseUp() //This adds undo if there is no selection, reason why this isn't in AfterUndoAdded,
         {   //is because it doesn't fire if no pixel changes were made.
-            if (_currentSelection.Length == 0)
+            if (_currentSelection != null && _currentSelection.Length == 0)
             {
                 UndoManager.AddUndoChange(new Change(ApplyOffsets, new object[]{_startingOffsets}, 
                     ApplyOffsets, new object[] { GetOffsets(_affectedLayers)}, "Move layers"));
@@ -183,7 +183,7 @@ namespace PixiEditor.Models.Tools.Tools
             if (!_clearedPixels.ContainsKey(layer) || _clearedPixels[layer] == false)
             {
                 ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.First(x => x == layer)
-                    .ApplyPixels(BitmapPixelChanges.FromSingleColoredArray(selection, System.Windows.Media.Colors.Transparent));
+                    .SetPixels(BitmapPixelChanges.FromSingleColoredArray(selection, System.Windows.Media.Colors.Transparent));
 
                 _clearedPixels[layer] = true;
             }

+ 1 - 2
PixiEditor/Models/Tools/Tools/PenTool.cs

@@ -33,8 +33,7 @@ namespace PixiEditor.Models.Tools.Tools
         {
             LineTool line = new LineTool();
             return BitmapPixelChanges.FromSingleColoredArray(
-                line.CreateLine(new[] { startingCoords, latestCords }, toolSize, 
-                    CapType.Square, CapType.Square), color);
+                line.CreateLine(startingCoords, latestCords, toolSize), color);
         }
     }
 }

+ 2 - 2
PixiEditor/Models/Tools/Tools/RectangleTool.cs

@@ -16,7 +16,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public RectangleTool()
         {
-            Tooltip = "Draws rectanlge on cavnas (R)";
+            Tooltip = "Draws rectangle on canvas (R). Hold Shift to draw square.";
         }
 
         public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
@@ -57,7 +57,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public Coordinates[] CreateRectangle(Coordinates start, Coordinates end, int thickness)
         {
-            return CreateRectangle(new[] {start, end}, thickness);
+            return CreateRectangle(new[] {end, start}, thickness);
         }
 
         private Coordinates[] CalculateRectanglePoints(DoubleCords coordinates)

+ 6 - 6
PixiEditor/Models/Tools/Tools/SelectTool.cs

@@ -48,7 +48,7 @@ namespace PixiEditor.Models.Tools.Tools
         private void Select(Coordinates[] pixels)
         {
             Coordinates[] selection = GetRectangleSelectionForPoints(pixels[^1], pixels[0]);
-            ViewModelMain.Current.ActiveSelection.SetSelection(selection.ToArray(), SelectionType);
+            ViewModelMain.Current.ActiveSelection.SetSelection(selection, SelectionType);
         }
 
         public Coordinates[] GetRectangleSelectionForPoints(Coordinates start, Coordinates end)
@@ -65,17 +65,17 @@ namespace PixiEditor.Models.Tools.Tools
         /// <returns>Coordinates array of pixels</returns>
         public Coordinates[] GetAllSelection()
         {
-            return GetAllSelection(ViewModelMain.Current.BitmapManager.ActiveDocument.Layers[0]);
+            return GetAllSelection(ViewModelMain.Current.BitmapManager.ActiveDocument);
         }
 
         /// <summary>
-        ///     Gets coordinates of every pixel in choosen layer
+        ///     Gets coordinates of every pixel in chosen document
         /// </summary>
-        /// <param name="layer"></param>
+        /// <param name="document"></param>
         /// <returns>Coordinates array of pixels</returns>
-        public Coordinates[] GetAllSelection(Layer layer)
+        public Coordinates[] GetAllSelection(Document document)
         {
-            return GetRectangleSelectionForPoints(new Coordinates(0, 0), new Coordinates(layer.Width, layer.Height));
+            return GetRectangleSelectionForPoints(new Coordinates(0, 0), new Coordinates(document.Width - 1, document.Height - 1));
         }
     }
 }

+ 3 - 5
PixiEditor/PixiEditor.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>WinExe</OutputType>
-    <TargetFramework>netcoreapp3.0</TargetFramework>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
     <UseWPF>true</UseWPF>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
@@ -13,6 +13,7 @@
     <PackageLicenseFile>LICENSE</PackageLicenseFile>
     <PackageIcon>icon.ico</PackageIcon>
     <ApplicationIcon>..\icon.ico</ApplicationIcon>
+    <Authors>Krzysztof Krysiński</Authors>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -47,11 +48,8 @@
     <PackageReference Include="Expression.Blend.Sdk">
       <Version>1.0.2</Version>
     </PackageReference>
-    <PackageReference Include="Extended.Wpf.Toolkit">
-      <Version>3.8.1</Version>
-    </PackageReference>
+    <PackageReference Include="Extended.Wpf.Toolkit" Version="3.8.2" />
     <PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
-    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="System.Drawing.Common" Version="4.7.0" />
     <PackageReference Include="WriteableBitmapEx">
       <Version>1.6.5</Version>

+ 3 - 3
PixiEditor/Properties/AssemblyInfo.cs

@@ -6,7 +6,7 @@ using System.Windows;
 // set of attributes. Change these attribute values to modify the information
 // associated with an assembly.
 [assembly: AssemblyTitle("PixiEditor")]
-[assembly: AssemblyDescription("Lighweighted Pixel Art editor.")]
+[assembly: AssemblyDescription("A lighweighted Pixel Art editor.")]
 [assembly: AssemblyConfiguration("")]
 [assembly: AssemblyCompany("")]
 [assembly: AssemblyProduct("PixiEditor")]
@@ -50,5 +50,5 @@ using System.Windows;
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 
-[assembly: AssemblyVersion("0.0.4.1")]
-[assembly: AssemblyFileVersion("0.0.4.1")]
+[assembly: AssemblyVersion("0.1.0.0")]
+[assembly: AssemblyFileVersion("0.1.0.0")]

+ 4 - 4
PixiEditor/ViewModels/ViewModelBase.cs

@@ -5,21 +5,21 @@ using System.Windows.Input;
 
 namespace PixiEditor.ViewModels
 {
-    internal class ViewModelBase : INotifyPropertyChanged
+    public class ViewModelBase : INotifyPropertyChanged
     {
         public event PropertyChangedEventHandler PropertyChanged = delegate { };
 
-        internal void RaisePropertyChanged(string property)
+        protected void RaisePropertyChanged(string property)
         {
             if (property != null) PropertyChanged(this, new PropertyChangedEventArgs(property));
         }
 
-        internal void CloseButton(object parameter)
+        protected void CloseButton(object parameter)
         {
             ((Window) parameter).Close();
         }
 
-        internal void DragMove(object parameter)
+        protected void DragMove(object parameter)
         {
             Window popup = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive);
             if (Mouse.LeftButton == MouseButtonState.Pressed) popup.DragMove();

+ 119 - 121
PixiEditor/ViewModels/ViewModelMain.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.ComponentModel;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Windows;
@@ -23,22 +24,16 @@ using PixiEditor.Models.Tools.Tools;
 
 namespace PixiEditor.ViewModels
 {
-    internal class ViewModelMain : ViewModelBase
+    public class ViewModelMain : ViewModelBase
     {
         private const string ConfirmationDialogMessage = "Document was modified. Do you want to save changes?";
 
-        private double _mouseXonCanvas;
-
-        private double _mouseYonCanvas;
-
-
         private Color _primaryColor = Colors.Black;
 
         private bool _recenterZoombox;
 
         private Color _secondaryColor = Colors.White;
 
-        private ToolType _selectedTool;
         private Selection _selection;
 
         private Cursor _toolCursor;
@@ -47,92 +42,6 @@ namespace PixiEditor.ViewModels
 
         private bool _unsavedDocumentModified;
 
-        public ViewModelMain()
-        {
-            FilesManager.InitializeTempDirectories();
-            BitmapManager = new BitmapManager();
-            BitmapManager.BitmapOperations.BitmapChanged += BitmapUtility_BitmapChanged;
-            BitmapManager.MouseController.StoppedRecordingChanges += MouseController_StoppedRecordingChanges;
-            BitmapManager.DocumentChanged += BitmapManager_DocumentChanged;
-            ChangesController = new PixelChangesController();
-            SelectToolCommand = new RelayCommand(SetTool, DocumentIsNotNull);
-            OpenNewFilePopupCommand = new RelayCommand(OpenNewFilePopup);
-            MouseMoveCommand = new RelayCommand(MouseMove);
-            MouseDownCommand = new RelayCommand(MouseDown);
-            SaveFileCommand = new RelayCommand(SaveFile, CanSave);
-            UndoCommand = new RelayCommand(Undo, CanUndo);
-            RedoCommand = new RelayCommand(Redo, CanRedo);
-            MouseUpCommand = new RelayCommand(MouseUp);
-            OpenFileCommand = new RelayCommand(Open);
-            SetActiveLayerCommand = new RelayCommand(SetActiveLayer);
-            NewLayerCommand = new RelayCommand(NewLayer, CanCreateNewLayer);
-            DeleteLayerCommand = new RelayCommand(DeleteLayer, CanDeleteLayer);
-            MoveToBackCommand = new RelayCommand(MoveLayerToBack, CanMoveToBack);
-            MoveToFrontCommand = new RelayCommand(MoveLayerToFront, CanMoveToFront);
-            SwapColorsCommand = new RelayCommand(SwapColors);
-            KeyDownCommand = new RelayCommand(KeyDown);
-            RenameLayerCommand = new RelayCommand(RenameLayer);
-            DeselectCommand = new RelayCommand(Deselect, SelectionIsNotEmpty);
-            SelectAllCommand = new RelayCommand(SelectAll, CanSelectAll);
-            CopyCommand = new RelayCommand(Copy, SelectionIsNotEmpty);
-            DuplicateCommand = new RelayCommand(Duplicate, SelectionIsNotEmpty);
-            CutCommand = new RelayCommand(Cut, SelectionIsNotEmpty);
-            PasteCommand = new RelayCommand(Paste, CanPaste);
-            ClipCanvasCommand = new RelayCommand(ClipCanvas, DocumentIsNotNull);
-            DeletePixelsCommand = new RelayCommand(DeletePixels, SelectionIsNotEmpty);
-            OpenResizePopupCommand = new RelayCommand(OpenResizePopup, DocumentIsNotNull);
-            SelectColorCommand = new RelayCommand(SelectColor);
-            RemoveSwatchCommand = new RelayCommand(RemoveSwatch);
-            SaveDocumentCommand = new RelayCommand(SaveDocument, DocumentIsNotNull);
-            OnStartupCommand = new RelayCommand(OnStartup);
-            CloseWindowCommand = new RelayCommand(CloseWindow);
-            CenterContentCommand = new RelayCommand(CenterContent, DocumentIsNotNull);
-            ToolSet = new ObservableCollection<Tool>
-            {
-                new MoveTool(), new PenTool(), new SelectTool(), new FloodFill(), new LineTool(),
-                new CircleTool(), new RectangleTool(), new EarserTool(), new ColorPickerTool(), new BrightnessTool()
-            };
-            ShortcutController = new ShortcutController
-            {
-                Shortcuts = new List<Shortcut>
-                {
-                    new Shortcut(Key.B, SelectToolCommand, ToolType.Pen),
-                    new Shortcut(Key.X, SwapColorsCommand),
-                    new Shortcut(Key.O, OpenFileCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.E, SelectToolCommand, ToolType.Earser),
-                    new Shortcut(Key.O, SelectToolCommand, ToolType.ColorPicker),
-                    new Shortcut(Key.R, SelectToolCommand, ToolType.Rectangle),
-                    new Shortcut(Key.C, SelectToolCommand, ToolType.Circle),
-                    new Shortcut(Key.L, SelectToolCommand, ToolType.Line),
-                    new Shortcut(Key.G, SelectToolCommand, ToolType.Bucket),
-                    new Shortcut(Key.U, SelectToolCommand, ToolType.Brightness),
-                    new Shortcut(Key.V, SelectToolCommand, ToolType.Move),
-                    new Shortcut(Key.M, SelectToolCommand, ToolType.Select),
-                    new Shortcut(Key.Y, RedoCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.Z, UndoCommand),
-                    new Shortcut(Key.S, SaveFileCommand,
-                        modifier: ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt),
-                    new Shortcut(Key.S, SaveDocumentCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.S, SaveDocumentCommand, "AsNew", ModifierKeys.Control | ModifierKeys.Shift),
-                    new Shortcut(Key.N, OpenNewFilePopupCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.D, DeselectCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.A, SelectAllCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.C, CopyCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.V, PasteCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.J, DuplicateCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.X, CutCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.Delete, DeletePixelsCommand),
-                    new Shortcut(Key.I, OpenResizePopupCommand, modifier: ModifierKeys.Control | ModifierKeys.Shift),
-                    new Shortcut(Key.C, OpenResizePopupCommand, "canvas", ModifierKeys.Control | ModifierKeys.Shift)
-                }
-            };
-            UndoManager.SetMainRoot(this);
-            ClipboardController = new ClipboardController();
-            SetActiveTool(ToolType.Move);
-            BitmapManager.PrimaryColor = PrimaryColor;
-            Current = this;
-        }
-
         public Action CloseAction { get; set; }
 
         public static ViewModelMain Current { get; set; }
@@ -141,7 +50,7 @@ namespace PixiEditor.ViewModels
         public RelayCommand MouseMoveCommand { get; set; } //Command that is used to draw
         public RelayCommand MouseDownCommand { get; set; }
         public RelayCommand KeyDownCommand { get; set; }
-        public RelayCommand SaveFileCommand { get; set; } //Command that is used to save file
+        public RelayCommand ExportFileCommand { get; set; } //Command that is used to save file
         public RelayCommand UndoCommand { get; set; }
         public RelayCommand RedoCommand { get; set; }
         public RelayCommand MouseUpCommand { get; set; }
@@ -168,6 +77,11 @@ namespace PixiEditor.ViewModels
         public RelayCommand OnStartupCommand { get; set; }
         public RelayCommand CloseWindowCommand { get; set; }
         public RelayCommand CenterContentCommand { get; set; }
+        public RelayCommand OpenHyperlinkCommand { get; set; }
+
+        private double _mouseXonCanvas;
+
+        private double _mouseYonCanvas;
 
         public double MouseXOnCanvas //Mouse X coordinate relative to canvas
         {
@@ -226,30 +140,16 @@ namespace PixiEditor.ViewModels
             }
         }
 
-        public ToolType SelectedTool
-        {
-            get => _selectedTool;
-            set
-            {
-                if (_selectedTool != value)
-                {
-                    _selectedTool = value;
-                    SetActiveTool(value);
-                    RaisePropertyChanged("SelectedTool");
-                }
-            }
-        }
-
         public ObservableCollection<Tool> ToolSet { get; set; }
 
-        public LayerChange[] UndoChanges
+        public LayerChange[] UndoChanges //This acts like UndoManager process, but it was implemented before process system, so it can be transformed into it
         {
             get => _undoChanges;
             set
             {
                 _undoChanges = value;
                 for (int i = 0; i < value.Length; i++)
-                    BitmapManager.ActiveDocument.Layers[value[i].LayerIndex].ApplyPixels(value[i].PixelChanges);
+                    BitmapManager.ActiveDocument.Layers[value[i].LayerIndex].SetPixels(value[i].PixelChanges);
             }
         }
 
@@ -278,7 +178,106 @@ namespace PixiEditor.ViewModels
             }
         }
 
-        public ClipboardController ClipboardController { get; set; }
+        public ViewModelMain()
+        {
+            BitmapManager = new BitmapManager();
+            BitmapManager.BitmapOperations.BitmapChanged += BitmapUtility_BitmapChanged;
+            BitmapManager.MouseController.StoppedRecordingChanges += MouseController_StoppedRecordingChanges;
+            BitmapManager.DocumentChanged += BitmapManager_DocumentChanged;
+            ChangesController = new PixelChangesController();
+            SelectToolCommand = new RelayCommand(SetTool, DocumentIsNotNull);
+            OpenNewFilePopupCommand = new RelayCommand(OpenNewFilePopup);
+            MouseMoveCommand = new RelayCommand(MouseMove);
+            MouseDownCommand = new RelayCommand(MouseDown);
+            ExportFileCommand = new RelayCommand(ExportFile, CanSave);
+            UndoCommand = new RelayCommand(Undo, CanUndo);
+            RedoCommand = new RelayCommand(Redo, CanRedo);
+            MouseUpCommand = new RelayCommand(MouseUp);
+            OpenFileCommand = new RelayCommand(Open);
+            SetActiveLayerCommand = new RelayCommand(SetActiveLayer);
+            NewLayerCommand = new RelayCommand(NewLayer, CanCreateNewLayer);
+            DeleteLayerCommand = new RelayCommand(DeleteLayer, CanDeleteLayer);
+            MoveToBackCommand = new RelayCommand(MoveLayerToBack, CanMoveToBack);
+            MoveToFrontCommand = new RelayCommand(MoveLayerToFront, CanMoveToFront);
+            SwapColorsCommand = new RelayCommand(SwapColors);
+            KeyDownCommand = new RelayCommand(KeyDown);
+            RenameLayerCommand = new RelayCommand(RenameLayer);
+            DeselectCommand = new RelayCommand(Deselect, SelectionIsNotEmpty);
+            SelectAllCommand = new RelayCommand(SelectAll, CanSelectAll);
+            CopyCommand = new RelayCommand(Copy, SelectionIsNotEmpty);
+            DuplicateCommand = new RelayCommand(Duplicate, SelectionIsNotEmpty);
+            CutCommand = new RelayCommand(Cut, SelectionIsNotEmpty);
+            PasteCommand = new RelayCommand(Paste, CanPaste);
+            ClipCanvasCommand = new RelayCommand(ClipCanvas, DocumentIsNotNull);
+            DeletePixelsCommand = new RelayCommand(DeletePixels, SelectionIsNotEmpty);
+            OpenResizePopupCommand = new RelayCommand(OpenResizePopup, DocumentIsNotNull);
+            SelectColorCommand = new RelayCommand(SelectColor);
+            RemoveSwatchCommand = new RelayCommand(RemoveSwatch);
+            SaveDocumentCommand = new RelayCommand(SaveDocument, DocumentIsNotNull);
+            OnStartupCommand = new RelayCommand(OnStartup);
+            CloseWindowCommand = new RelayCommand(CloseWindow);
+            CenterContentCommand = new RelayCommand(CenterContent, DocumentIsNotNull);
+            OpenHyperlinkCommand = new RelayCommand(OpenHyperlink);
+            ToolSet = new ObservableCollection<Tool>
+            {
+                new MoveTool(), new PenTool(), new SelectTool(), new FloodFill(), new LineTool(),
+                new CircleTool(), new RectangleTool(), new EarserTool(), new ColorPickerTool(), new BrightnessTool()
+            };
+            ShortcutController = new ShortcutController
+            {
+                Shortcuts = new List<Shortcut>
+                {
+                    //Tools
+                    new Shortcut(Key.B, SelectToolCommand, ToolType.Pen),
+                    new Shortcut(Key.E, SelectToolCommand, ToolType.Earser),
+                    new Shortcut(Key.O, SelectToolCommand, ToolType.ColorPicker),
+                    new Shortcut(Key.R, SelectToolCommand, ToolType.Rectangle),
+                    new Shortcut(Key.C, SelectToolCommand, ToolType.Circle),
+                    new Shortcut(Key.L, SelectToolCommand, ToolType.Line),
+                    new Shortcut(Key.G, SelectToolCommand, ToolType.Bucket),
+                    new Shortcut(Key.U, SelectToolCommand, ToolType.Brightness),
+                    new Shortcut(Key.V, SelectToolCommand, ToolType.Move),
+                    new Shortcut(Key.M, SelectToolCommand, ToolType.Select),
+                    //Editor
+                    new Shortcut(Key.X, SwapColorsCommand),
+                    new Shortcut(Key.Y, RedoCommand, modifier: ModifierKeys.Control),
+                    new Shortcut(Key.Z, UndoCommand),
+                    new Shortcut(Key.D, DeselectCommand, modifier: ModifierKeys.Control),
+                    new Shortcut(Key.A, SelectAllCommand, modifier: ModifierKeys.Control),
+                    new Shortcut(Key.C, CopyCommand, modifier: ModifierKeys.Control),
+                    new Shortcut(Key.V, PasteCommand, modifier: ModifierKeys.Control),
+                    new Shortcut(Key.J, DuplicateCommand, modifier: ModifierKeys.Control),
+                    new Shortcut(Key.X, CutCommand, modifier: ModifierKeys.Control),
+                    new Shortcut(Key.Delete, DeletePixelsCommand),
+                    new Shortcut(Key.I, OpenResizePopupCommand, modifier: ModifierKeys.Control | ModifierKeys.Shift),
+                    new Shortcut(Key.C, OpenResizePopupCommand, "canvas", ModifierKeys.Control | ModifierKeys.Shift),
+                    //File
+                    new Shortcut(Key.O, OpenFileCommand, modifier: ModifierKeys.Control),
+                    new Shortcut(Key.S, ExportFileCommand,
+                        modifier: ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt),
+                    new Shortcut(Key.S, SaveDocumentCommand, modifier: ModifierKeys.Control),
+                    new Shortcut(Key.S, SaveDocumentCommand, "AsNew", ModifierKeys.Control | ModifierKeys.Shift),
+                    new Shortcut(Key.N, OpenNewFilePopupCommand, modifier: ModifierKeys.Control),
+                }
+            };
+            UndoManager.SetMainRoot(this);
+            SetActiveTool(ToolType.Move);
+            BitmapManager.PrimaryColor = PrimaryColor;
+            ActiveSelection = new Selection(Array.Empty<Coordinates>());
+            Current = this;
+        }
+
+        private void OpenHyperlink(object parameter)
+        {
+            if (parameter == null) return;
+            string url = (string) parameter;
+            var processInfo = new ProcessStartInfo()
+            {
+                FileName = url,
+                UseShellExecute = true
+            };
+            Process.Start(processInfo);
+        }
 
         private void CenterContent(object property)
         {
@@ -363,7 +362,7 @@ namespace PixiEditor.ViewModels
         {
             bool paramIsAsNew = parameter != null && parameter.ToString()?.ToLower() == "asnew";
             if (paramIsAsNew || Exporter.SaveDocumentPath == null)
-                Exporter.SaveAsNewEditableFile(BitmapManager.ActiveDocument, !paramIsAsNew);
+                Exporter.SaveAsEditableFileWithDialog(BitmapManager.ActiveDocument, !paramIsAsNew);
             else
                 Exporter.SaveAsEditableFile(BitmapManager.ActiveDocument, Exporter.SaveDocumentPath);
             _unsavedDocumentModified = false;
@@ -429,7 +428,7 @@ namespace PixiEditor.ViewModels
         public void Cut(object parameter)
         {
             Copy(null);
-            BitmapManager.ActiveLayer.ApplyPixels(
+            BitmapManager.ActiveLayer.SetPixels(
                 BitmapPixelChanges.FromSingleColoredArray(ActiveSelection.SelectedPoints.ToArray(),
                     Colors.Transparent));
         }
@@ -447,13 +446,13 @@ namespace PixiEditor.ViewModels
         private void Copy(object parameter)
         {
             ClipboardController.CopyToClipboard(BitmapManager.ActiveDocument.Layers.ToArray(),
-                ActiveSelection.SelectedPoints.ToArray());
+                ActiveSelection.SelectedPoints.ToArray(), BitmapManager.ActiveDocument.Width, BitmapManager.ActiveDocument.Height);
         }
 
         public void SelectAll(object parameter)
         {
             SelectTool select = new SelectTool();
-            select.Use(select.GetAllSelection());
+            ActiveSelection.SetSelection(select.GetAllSelection(), SelectionType.New);
         }
 
         private bool CanSelectAll(object property)
@@ -461,7 +460,7 @@ namespace PixiEditor.ViewModels
             return BitmapManager.ActiveDocument != null && BitmapManager.ActiveDocument.Layers.Count > 0;
         }
 
-        private bool DocumentIsNotNull(object property)
+        public bool DocumentIsNotNull(object property)
         {
             return BitmapManager.ActiveDocument != null;
         }
@@ -473,8 +472,7 @@ namespace PixiEditor.ViewModels
 
         private bool SelectionIsNotEmpty(object property)
         {
-            return ActiveSelection != null && ActiveSelection.SelectedPoints != null &&
-                   ActiveSelection.SelectedPoints.Count > 0;
+            return ActiveSelection?.SelectedPoints != null && ActiveSelection.SelectedPoints.Count > 0;
         }
 
         public void SetTool(object parameter)
@@ -615,7 +613,7 @@ namespace PixiEditor.ViewModels
         /// <param name="parameter"></param>
         private void MouseMove(object parameter)
         {
-            Coordinates cords = new Coordinates((int) MouseXOnCanvas, (int) MouseYOnCanvas);
+            Coordinates cords = new Coordinates((int)MouseXOnCanvas, (int)MouseYOnCanvas);
             MousePositionConverter.CurrentCoordinates = cords;
 
 
@@ -653,7 +651,7 @@ namespace PixiEditor.ViewModels
             }
         }
 
-        private void NewDocument(int width, int height, bool addBaseLayer = true)
+        public void NewDocument(int width, int height, bool addBaseLayer = true)
         {
             BitmapManager.ActiveDocument = new Document(width, height);
             if(addBaseLayer)
@@ -734,7 +732,7 @@ namespace PixiEditor.ViewModels
         ///     Generates export dialog or saves directly if save data is known.
         /// </summary>
         /// <param name="parameter"></param>
-        private void SaveFile(object parameter)
+        private void ExportFile(object parameter)
         {
             WriteableBitmap bitmap = BitmapManager.GetCombinedLayersBitmap();
             Exporter.Export(bitmap, new Size(bitmap.PixelWidth, bitmap.PixelHeight));

+ 1 - 1
PixiEditor/Views/ImportFilePopup.xaml

@@ -42,7 +42,7 @@
                 <StackPanel Background="{StaticResource MainColor}" Height="120" Width="225" Margin="0,30,0,0">
                     <local:SizePicker EditingEnabled="{Binding PathIsCorrect}"
                                       ChosenWidth="{Binding ImportWidth, Mode=TwoWay}"
-                                      ChoosenHeight="{Binding ImportHeight,Mode=TwoWay}" />
+                                      ChosenHeight="{Binding ImportHeight,Mode=TwoWay}" />
                 </StackPanel>
             </StackPanel>
             <Button Grid.Row="1" Height="30" Width="60" VerticalAlignment="Bottom" HorizontalAlignment="Right"

+ 24 - 5
PixiEditor/Views/MainWindow.xaml

@@ -10,7 +10,7 @@
         xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
         xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
         xmlns:ui="clr-namespace:PixiEditor.Helpers.UI"
-        xmlns:cmd="http://www.galasoft.ch/mvvmlight" xmlns:avalondock="https://github.com/Dirkster99/AvalonDock"
+        xmlns:cmd="http://www.galasoft.ch/mvvmlight" xmlns:avalondock="https://github.com/Dirkster99/AvalonDock" xmlns:position="clr-namespace:PixiEditor.Models.Position"
         mc:Ignorable="d" WindowStyle="None"
         Title="PixiEditor" Name="mainWindow" Height="1000" Width="1600" Background="{StaticResource MainColor}"
         WindowStartupLocation="CenterScreen" WindowState="Maximized" DataContext="{DynamicResource ViewModelMain}">
@@ -22,10 +22,9 @@
     <Window.Resources>
         <vm:ViewModelMain x:Key="ViewModelMain" />
         <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
-        <helpers:ToolSizeToIntConverter x:Key="ToolSizeToIntConverter" />
-        <converters:BoolToColorConverter x:Key="BoolToColorConverter" />
         <converters:BoolToIntConverter x:Key="BoolToIntConverter" />
         <converters:FloatNormalizeConverter x:Key="FloatNormalizeConverter" />
+        <converters:DoubleToIntConverter x:Key="DoubleToIntConverter"/>
     </Window.Resources>
 
     <Window.CommandBindings>
@@ -77,7 +76,7 @@
                     <MenuItem Header="_Save" InputGestureText="Ctrl+S" Command="{Binding SaveDocumentCommand}" />
                     <MenuItem Header="_Save As..." InputGestureText="Ctrl+Shift+S"
                               Command="{Binding SaveDocumentCommand}" CommandParameter="AsNew" />
-                    <MenuItem Header="_Export" InputGestureText="Ctrl+Shift+Alt+S" Command="{Binding SaveFileCommand}" />
+                    <MenuItem Header="_Export" InputGestureText="Ctrl+Shift+Alt+S" Command="{Binding ExportFileCommand}" />
                 </MenuItem>
                 <MenuItem Header="_Edit">
                     <MenuItem Header="_Undo" InputGestureText="Ctrl+Z" Command="{Binding UndoCommand}" />
@@ -104,6 +103,19 @@
                     <Separator/>
                     <MenuItem Header="Center Content" Command="{Binding CenterContentCommand}" />
                 </MenuItem>
+                <MenuItem Header="_Help">
+                    <MenuItem Header="Documentation" Command="{Binding OpenHyperlinkCommand}"
+                              CommandParameter="https://github.com/flabbet/PixiEditor/wiki"/>
+                    <MenuItem Header="Repository" Command="{Binding OpenHyperlinkCommand}"
+                              CommandParameter="https://github.com/flabbet/PixiEditor"/>
+                    <MenuItem Header="Shortcuts" Command="{Binding OpenHyperlinkCommand}"
+                              CommandParameter="https://github.com/flabbet/PixiEditor/wiki/Shortcuts"/>
+                    <Separator/>
+                    <MenuItem Header="License" Command="{Binding OpenHyperlinkCommand}"
+                              CommandParameter="https://github.com/flabbet/PixiEditor/blob/master/LICENSE"/>
+                    <MenuItem Header="Third Party Licenses" Command="{Binding OpenHyperlinkCommand}"
+                              CommandParameter="https://github.com/flabbet/PixiEditor/wiki/Third-party-licenses"/>
+                </MenuItem>
             </Menu>
             <StackPanel DockPanel.Dock="Right" VerticalAlignment="Top" Orientation="Horizontal"
                         HorizontalAlignment="Right" WindowChrome.IsHitTestVisibleInChrome="True">
@@ -366,6 +378,13 @@
                 </avalondock:DockingManager.Theme>
             </avalondock:DockingManager>
         </Grid>
-
+        <DockPanel Grid.Row="3" Grid.Column="1">
+            <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
+                <TextBlock Text="X:" Foreground="White" FontSize="16"/>
+                <TextBlock Margin="4,0,10,0" Text="{Binding MouseXOnCanvas, Converter={StaticResource DoubleToIntConverter}}" Foreground="White" FontSize="16"/>
+                <TextBlock Text="Y:" Foreground="White" FontSize="16"/>
+                <TextBlock Margin="4,0,10,0" Text="{Binding MouseYOnCanvas, Converter={StaticResource DoubleToIntConverter}}" Foreground="White" FontSize="16"/>
+            </StackPanel>
+        </DockPanel>
     </Grid>
 </Window>

+ 1 - 1
PixiEditor/Views/NewFilePopup.xaml

@@ -38,7 +38,7 @@
             <StackPanel HorizontalAlignment="Center" Margin="0,60,0,0" Background="{StaticResource MainColor}"
                         VerticalAlignment="Top" Grid.Row="1" Width="350" Height="150">
                 <local:SizePicker Margin="0,20" HorizontalAlignment="Center" Height="110"
-                                  ChoosenHeight="{Binding FileHeight,Mode=TwoWay, ElementName=newFilePopup}"
+                                  ChosenHeight="{Binding FileHeight,Mode=TwoWay, ElementName=newFilePopup}"
                                   ChosenWidth="{Binding FileWidth,Mode=TwoWay, ElementName=newFilePopup}" />
             </StackPanel>
             <Button VerticalAlignment="Bottom" HorizontalAlignment="Right" FontSize="20" Height="30" Width="60"

+ 1 - 1
PixiEditor/Views/ResizeCanvasPopup.xaml

@@ -41,7 +41,7 @@
         <StackPanel HorizontalAlignment="Center" Margin="0,50,0,0" Background="{StaticResource MainColor}"
                     VerticalAlignment="Top" Grid.Row="1" Width="300" Height="250">
             <local:SizePicker Margin="0,10,0,0" Width="300" Height="110"
-                              ChoosenHeight="{Binding NewHeight, Mode=TwoWay, ElementName=window}"
+                              ChosenHeight="{Binding NewHeight, Mode=TwoWay, ElementName=window}"
                               ChosenWidth="{Binding NewWidth, Mode=TwoWay, ElementName=window}" />
             <Separator Margin="10,20,10,0" Background="{StaticResource AccentColor}" Height="1" />
             <Label Content="Anchor point:" Foreground="White" Margin="10,5,0,0" HorizontalAlignment="Left"

+ 1 - 1
PixiEditor/Views/ResizeDocumentPopup.xaml

@@ -39,7 +39,7 @@
                Content="Resize document" />
         <StackPanel HorizontalAlignment="Center" Margin="0,50,0,0" Background="{StaticResource MainColor}"
                     VerticalAlignment="Top" Grid.Row="1" Width="350" Height="150">
-            <local:SizePicker Margin="0,20" ChoosenHeight="{Binding Path=NewHeight, Mode=TwoWay, ElementName=window}"
+            <local:SizePicker Margin="0,20" ChosenHeight="{Binding Path=NewHeight, Mode=TwoWay, ElementName=window}"
                               ChosenWidth="{Binding Path=NewWidth, Mode=TwoWay, ElementName=window}" />
         </StackPanel>
         <Button Grid.Row="1" Height="30" Width="60" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="10"

+ 1 - 1
PixiEditor/Views/SaveFilePopup.xaml

@@ -34,7 +34,7 @@
                        Text="File settings" TextAlignment="Center" Margin="0,10,0,0" FontSize="24" />
             <StackPanel Orientation="Vertical" Grid.Row="1" Margin="0,50,0,0">
                 <local:SizePicker Width="250" Height="120"
-                                  ChoosenHeight="{Binding Path=SaveHeight, Mode=TwoWay, ElementName=saveFilePopup}"
+                                  ChosenHeight="{Binding Path=SaveHeight, Mode=TwoWay, ElementName=saveFilePopup}"
                                   ChosenWidth="{Binding Path=SaveWidth, Mode=TwoWay, ElementName=saveFilePopup}" />
                 <Button Foreground="Snow" Height="40" Width="160" Margin="0,10,0,0" Content="Path"
                         Background="{StaticResource MainColor}" BorderBrush="{Binding PathButtonBorder}"

+ 10 - 10
PixiEditor/Views/SizePicker.xaml

@@ -7,24 +7,24 @@
              mc:Ignorable="d"
              d:DesignHeight="110" d:DesignWidth="215" Name="uc">
     <StackPanel Background="{StaticResource MainColor}">
-        <DockPanel Margin="5,10,0,0" HorizontalAlignment="Center" VerticalAlignment="Center">
-            <TextBlock Height="30" Foreground="Snow" Text="Height:" TextAlignment="Center" FontSize="16"
-                       HorizontalAlignment="Center" VerticalAlignment="Center" />
-            <local:SizeInput IsEnabled="{Binding EditingEnabled, ElementName=uc}" Margin="5,0,0,0"
-                             PreserveAspectRatio="{Binding Path=IsChecked, ElementName=aspectRatio}"
-                             AspectRatioValue="{Binding Path=ChosenWidth, ElementName=uc}"
-                             HorizontalAlignment="Left" Width="150" Height="30"
-                             Size="{Binding ChoosenHeight, ElementName=uc, Mode=TwoWay}" />
-        </DockPanel>
         <DockPanel Margin="5,10,0,0" HorizontalAlignment="Center" VerticalAlignment="Center">
             <TextBlock Height="30" Foreground="Snow" Text="Width:" TextAlignment="Center" FontSize="16"
                        HorizontalAlignment="Center" VerticalAlignment="Center" />
             <local:SizeInput IsEnabled="{Binding EditingEnabled, ElementName=uc}" Width="150" Height="30"
                              PreserveAspectRatio="{Binding Path=IsChecked, ElementName=aspectRatio}"
-                             AspectRatioValue="{Binding Path=ChoosenHeight, ElementName=uc}"
+                             AspectRatioValue="{Binding Path=ChosenHeight, ElementName=uc}"
                              HorizontalAlignment="Left" Margin="10,0,0,0"
                              Size="{Binding Path=ChosenWidth, ElementName=uc, Mode=TwoWay}" />
         </DockPanel>
+        <DockPanel Margin="5,10,0,0" HorizontalAlignment="Center" VerticalAlignment="Center">
+            <TextBlock Height="30" Foreground="Snow" Text="Height:" TextAlignment="Center" FontSize="16"
+                       HorizontalAlignment="Center" VerticalAlignment="Center" />
+            <local:SizeInput IsEnabled="{Binding EditingEnabled, ElementName=uc}" Margin="5,0,0,0"
+                             PreserveAspectRatio="{Binding Path=IsChecked, ElementName=aspectRatio}"
+                             AspectRatioValue="{Binding Path=ChosenWidth, ElementName=uc}"
+                             HorizontalAlignment="Left" Width="150" Height="30"
+                             Size="{Binding ChosenHeight, ElementName=uc, Mode=TwoWay}" />
+        </DockPanel>
         <CheckBox Name="aspectRatio" Content="Preserve aspect ratio" Foreground="White" HorizontalAlignment="Left"
                   IsChecked="True" Margin="50,10,0,0" />
     </StackPanel>

+ 6 - 6
PixiEditor/Views/SizePicker.xaml.cs

@@ -16,9 +16,9 @@ namespace PixiEditor.Views
         public static readonly DependencyProperty ChosenWidthProperty =
             DependencyProperty.Register("ChosenWidth", typeof(int), typeof(SizePicker), new PropertyMetadata(1));
 
-        // Using a DependencyProperty as the backing store for ChoosenHeight.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty ChoosenHeightProperty =
-            DependencyProperty.Register("ChoosenHeight", typeof(int), typeof(SizePicker), new PropertyMetadata(1));
+        // Using a DependencyProperty as the backing store for ChosenHeight.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty ChosenHeightProperty =
+            DependencyProperty.Register("ChosenHeight", typeof(int), typeof(SizePicker), new PropertyMetadata(1));
 
         public SizePicker()
         {
@@ -40,10 +40,10 @@ namespace PixiEditor.Views
         }
 
 
-        public int ChoosenHeight
+        public int ChosenHeight
         {
-            get => (int) GetValue(ChoosenHeightProperty);
-            set => SetValue(ChoosenHeightProperty, value);
+            get => (int) GetValue(ChosenHeightProperty);
+            set => SetValue(ChosenHeightProperty, value);
         }
     }
 }

+ 12 - 0
PixiEditorTests/ApplicationCollection.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+namespace PixiEditorTests
+{
+    [CollectionDefinition("Application collection")]
+    public class ApplicationCollection : ICollectionFixture<ApplicationFixture>
+    {
+    }
+}

+ 22 - 0
PixiEditorTests/ApplicationFixture.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using System.Windows;
+using PixiEditor;
+
+namespace PixiEditorTests
+{
+    [ExcludeFromCodeCoverage]
+    public class ApplicationFixture
+    {
+        public ApplicationFixture()
+        {
+            if (Application.Current == null)
+            {
+                var app = new App();
+                app.InitializeComponent();
+            }
+        }
+    }
+}

+ 23 - 0
PixiEditorTests/HelpersTests/ConvertersTests/DoubleToIntConverterTest.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using PixiEditor.Helpers.Converters;
+using Xunit;
+
+namespace PixiEditorTests.HelpersTests.ConvertersTests
+{
+    public class DoubleToIntConverterTest
+    {
+        [Fact]
+        public void TestThatConvertConvertsDoubleToInt()
+        {
+            DoubleToIntConverter converter = new DoubleToIntConverter();
+
+            var value = converter.Convert(5.123, typeof(int), null, CultureInfo.CurrentCulture);
+
+            Assert.IsType<int>(value);
+            Assert.Equal(5, (int)value);
+        }
+    }
+}

+ 3 - 3
PixiEditorTests/WorkspaceTests/ColorsTests/ExtendedColorTests.cs → PixiEditorTests/ModelsTests/ColorsTests/ExtendedColorTests.cs

@@ -1,9 +1,9 @@
-using PixiEditor.Models.Colors;
-using System;
+using System;
 using System.Windows.Media;
+using PixiEditor.Models.Colors;
 using Xunit;
 
-namespace PixiEditorTests.WorkspaceTests.ColorsTests
+namespace PixiEditorTests.ModelsTests.ColorsTests
 {
     public class ExtendedColorTests
     {

+ 28 - 9
PixiEditorTests/ModelsTests/ControllersTests/BitmapManagerTests.cs

@@ -10,6 +10,7 @@ using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Models.Tools.ToolSettings;
 using Xunit;
 
 namespace PixiEditorTests.ModelsTests.ControllersTests
@@ -21,7 +22,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         public void TestThatBitmapManagerSetsCorrectTool()
         {
              BitmapManager bitmapManager = new BitmapManager();
-             bitmapManager.SetActiveTool(new MockedPen());
+             bitmapManager.SetActiveTool(new MockedSinglePixelPen());
              Assert.Equal(ToolType.Pen, bitmapManager.SelectedTool.ToolType);
         }
 
@@ -71,21 +72,39 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         [Fact]
         public void TestThatIsOperationToolWorks()
         {
-            MockedPen pen = new MockedPen();
-            Assert.True(BitmapManager.IsOperationTool(pen));
+            MockedSinglePixelPen singlePixelPen = new MockedSinglePixelPen();
+            Assert.True(BitmapManager.IsOperationTool(singlePixelPen));
+        }
+
+        [StaFact]
+        public void TestThatBitmapChangesExecuteToolExecutesPenTool()
+        {
+            BitmapManager bitmapManager = new BitmapManager
+            {
+                ActiveDocument = new Document(5, 5)
+            };
+
+            bitmapManager.AddNewLayer("Layer");
+            bitmapManager.SetActiveTool(new MockedSinglePixelPen());
+            bitmapManager.PrimaryColor = Colors.Green;
+
+            bitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
+            bitmapManager.MouseController.RecordMouseMovementChange(new Coordinates(1, 1));
+            bitmapManager.MouseController.StopRecordingMouseMovementChanges();
+
+            bitmapManager.ExecuteTool(new Coordinates(1, 1));
+
+            Assert.Equal(Colors.Green, bitmapManager.ActiveLayer.GetPixelWithOffset(1, 1));
         }
 
     }
 
-    public class MockedPen : BitmapOperationTool
+    public class MockedSinglePixelPen : BitmapOperationTool
     {
         public override LayerChange[] Use(Layer layer, Coordinates[] mouseMove, Color color)
         {
-            PenTool pen = new PenTool()
-            {
-                Toolbar = null
-            };
-           return pen.Use(layer, mouseMove, color);
+            return Only(
+                BitmapPixelChanges.FromSingleColoredArray(new[] {mouseMove[0]}, color),0);
         }
 
         public override ToolType ToolType { get; } = ToolType.Pen;

+ 50 - 0
PixiEditorTests/ModelsTests/ControllersTests/BitmapOperationsUtilityTests.cs

@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.Windows.Media;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ControllersTests
+{
+    public class BitmapOperationsUtilityTests
+    {
+
+        [Fact]
+        public void TestThatBitmapOperationsUtilityDeletesPixels()
+        {
+            BitmapOperationsUtility util = new BitmapOperationsUtility(new BitmapManager());
+
+            Layer testLayer = new Layer("test layer", 10, 10);
+            Coordinates[] cords = {new Coordinates(0, 0), new Coordinates(1, 1)};
+            BitmapPixelChanges pixels = BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Black);
+            testLayer.SetPixels(pixels);
+
+            util.DeletePixels(new []{testLayer}, cords);
+
+            Assert.Equal(0, testLayer.GetPixel(0, 0).A);
+            Assert.Equal(0, testLayer.GetPixel(1, 1).A);
+        }
+
+        [StaFact]
+        public void TestThatBitmapOperationsUtilityExecutesPenToolProperly()
+        {
+            BitmapManager manager = new BitmapManager
+            {
+                ActiveDocument = new Document(10, 10),
+                PrimaryColor = Colors.Black,
+            };
+            manager.AddNewLayer("Test layer", 10, 10);
+
+            BitmapOperationsUtility util = new BitmapOperationsUtility(manager);
+
+            List<Coordinates> mouseMove = new List<Coordinates>(new [] {new Coordinates(0,0)});
+
+            util.ExecuteTool(new Coordinates(0,0), mouseMove, new MockedSinglePixelPen());
+            Assert.Equal(manager.ActiveLayer.GetPixel(0,0), Colors.Black);
+        }
+
+    }
+}

+ 128 - 0
PixiEditorTests/ModelsTests/ControllersTests/ClipboardControllerTests.cs

@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ControllersTests
+{
+    public class ClipboardControllerTests
+    {
+        private Color testColor = Colors.Coral;
+
+        [StaFact]
+        public void TestThatClipboardControllerIgnoresNonImageDataInClipboard()
+        {
+            Clipboard.Clear();
+            Clipboard.SetText("Text data");
+            var img = ClipboardController.GetImageFromClipboard();
+            Assert.Null(img);
+        }
+
+        [StaFact]
+        public void TestThatIsImageInClipboardWorksForDib()
+        {
+            Clipboard.Clear();
+            Clipboard.SetImage(BitmapFactory.New(10, 10));
+            Assert.True(ClipboardController.IsImageInClipboard());
+        }
+
+        [StaFact]
+        public void TestThatClipboardControllerSavesImageToClipboard()
+        {
+            Layer testLayer = new Layer("test layer", 10, 10);
+            ClipboardController.CopyToClipboard(new []{testLayer}, CoordinatesCalculator.RectangleToCoordinates(0,0, 9,9), 10, 10);
+            Assert.True(ClipboardController.IsImageInClipboard());
+        }
+
+        [StaFact]
+        public void TestThatCopyToClipboardWithSelectionSavesCorrectBitmap()
+        {
+            Clipboard.Clear();
+
+            Layer testLayer = new Layer("test layer", 10, 10);
+            Layer testLayer2 = new Layer("test layer", 10, 10);
+            testLayer.SetPixel(new Coordinates(4,4), testColor);
+            testLayer2.SetPixel(new Coordinates(5,5), testColor);
+
+            ClipboardController.CopyToClipboard(new []{testLayer, testLayer2}, 
+                new []{new Coordinates(4,4), new Coordinates(5,5)}, 10, 10);
+
+            var img = Clipboard.GetImage(); // Using default Clipboard get image to avoid false positives from faulty ClipboardController GetImage
+
+            Assert.True(ClipboardController.IsImageInClipboard());
+            Assert.NotNull(img);
+            Assert.Equal(2, img.PixelWidth);
+            Assert.Equal(2, img.PixelHeight);
+
+            var bmp = new WriteableBitmap(img);
+            Assert.Equal(testColor, bmp.GetPixel(0, 0));
+            Assert.Equal(testColor, bmp.GetPixel(1, 1));
+        }
+
+        [StaFact]
+        public void TestThatClipboardControllerGetsCorrectImageInDibFormatFromClipboard()
+        {
+            Clipboard.Clear();
+            var bmp = BitmapFactory.New(10, 10);
+            bmp.SetPixel(4, 4, testColor);
+            Clipboard.SetImage(bmp);
+
+            var img = ClipboardController.GetImageFromClipboard();
+            Assert.NotNull(img);
+            Assert.Equal(10, img.PixelWidth);
+            Assert.Equal(10, img.PixelHeight);
+            Assert.Equal(testColor, bmp.GetPixel(4, 4));
+        }
+
+        [StaFact]
+        public void TestThatClipboardControllerGetsCorrectImageInPngFormatFromClipboard()
+        {
+            Clipboard.Clear();
+            var bmp = BitmapFactory.New(10, 10);
+            bmp.SetPixel(4,4, testColor);
+            using (var pngStream = new MemoryStream())
+            {
+                DataObject data = new DataObject();
+
+                PngBitmapEncoder encoder = new PngBitmapEncoder();
+                encoder.Frames.Add(BitmapFrame.Create(bmp));
+                encoder.Save(pngStream);
+                data.SetData("PNG", pngStream, false); //PNG, supports transparency
+                Clipboard.SetDataObject(data, true);
+            }
+
+            var img = ClipboardController.GetImageFromClipboard();
+            Assert.NotNull(img);
+            Assert.Equal(10, img.PixelWidth);
+            Assert.Equal(10, img.PixelHeight);
+            Assert.Equal(testColor, bmp.GetPixel(4,4));
+        }
+
+        [StaFact]
+        public void TestThatClipboardControllerGetsCorrectImageInBitmapFormatFromClipboard()
+        {
+            Clipboard.Clear();
+            var bmp = BitmapFactory.New(10, 10);
+            bmp.SetPixel(4, 4, testColor);
+
+            DataObject data = new DataObject();
+            data.SetData(DataFormats.Bitmap, bmp, false); //PNG, supports transparency
+            Clipboard.SetDataObject(data, true);
+
+            var img = ClipboardController.GetImageFromClipboard();
+            Assert.NotNull(img);
+            Assert.Equal(10, img.PixelWidth);
+            Assert.Equal(10, img.PixelHeight);
+            Assert.Equal(testColor, bmp.GetPixel(4, 4));
+        }
+
+        
+    }
+}

+ 80 - 0
PixiEditorTests/ModelsTests/ControllersTests/MouseMovementControllerTests.cs

@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Position;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ControllersTests
+{
+    public class MouseMovementControllerTests
+    {
+        [Fact]
+        public void TestThatStartRecordingMouseMovChangesStartsRecordingAndInvokesEvent()
+        {
+            bool eventInvoked = false;
+            MouseMovementController controller = new MouseMovementController();
+            controller.StartedRecordingChanges += (sender, e) => eventInvoked = true;
+
+            controller.StartRecordingMouseMovementChanges(false);
+
+            Assert.True(controller.IsRecordingChanges);
+            Assert.True(eventInvoked);
+            Assert.False(controller.ClickedOnCanvas);
+        }
+
+        [Fact]
+        public void TestThatRecordMouseMovementChangeRecordsMouseMovementChange()
+        {
+            MouseMovementController controller = new MouseMovementController();
+            controller.StartRecordingMouseMovementChanges(false);
+            controller.RecordMouseMovementChange(new Coordinates(5,5));
+
+            Assert.NotEmpty(controller.LastMouseMoveCoordinates);
+            Assert.Equal(new Coordinates(5,5),controller.LastMouseMoveCoordinates[0]);
+            Assert.True(controller.IsRecordingChanges);
+        }
+
+        [Fact]
+        public void TestThatMouseMovedRaisesEvent()
+        {
+            bool eventRaised = false;
+            Coordinates position = new Coordinates(5,5);
+            MouseMovementEventArgs args = new MouseMovementEventArgs(new Coordinates());
+
+            MouseMovementController controller = new MouseMovementController();
+            controller.MousePositionChanged += (s, e) =>
+            {
+                eventRaised = true;
+                args = e;
+            };
+
+            controller.MouseMoved(position);
+
+            Assert.True(eventRaised);
+            Assert.Equal(position, args.NewPosition);
+        }
+
+        [Fact]
+        public void TestStopRecordingChangesStopsRecording()
+        {
+            MouseMovementController controller = new MouseMovementController();
+
+            controller.StartRecordingMouseMovementChanges(true);
+            controller.StopRecordingMouseMovementChanges();
+
+            Assert.False(controller.IsRecordingChanges);
+            Assert.False(controller.ClickedOnCanvas);
+        }
+
+        [Fact]
+        public void TestThatRecordChangesNotRecords()
+        {
+            MouseMovementController controller = new MouseMovementController();
+            controller.RecordMouseMovementChange(new Coordinates(5,10));
+
+            Assert.False(controller.IsRecordingChanges);
+            Assert.Empty(controller.LastMouseMoveCoordinates);
+        }
+    }
+}

+ 65 - 0
PixiEditorTests/ModelsTests/ControllersTests/PixelChangesControllerTests.cs

@@ -0,0 +1,65 @@
+using System.Windows.Media;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ControllersTests
+{
+    public class PixelChangesControllerTests
+    {
+
+        [Fact]
+        public void TestThatPopChangesPopsChanges()
+        {
+            var controller = CreateBasicController();
+
+            var changes = controller.PopChanges();
+            Assert.NotEmpty(changes);
+            Assert.Null(controller.PopChanges());
+        }
+
+        [Fact]
+        public void TestThatAddChangesAddsAsNewChange()
+        {
+            var controller = CreateBasicController();
+            Coordinates[] cords = { new Coordinates(5, 3), new Coordinates(7, 2) };
+
+            controller.AddChanges(new LayerChange(
+                    BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Black), 1),
+                new LayerChange(BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Transparent), 1));
+
+            var changes = controller.PopChanges();
+            Assert.Equal(2, changes.Length);
+        }
+
+        [Fact]
+        public void TestThatAddChangesAddsToExistingChange()
+        {
+            Coordinates[] cords2 = { new Coordinates(2, 2), new Coordinates(5, 5) };
+            var controller = CreateBasicController();
+
+            controller.AddChanges(new LayerChange(
+                    BitmapPixelChanges.FromSingleColoredArray(cords2, Colors.Black), 0),
+                new LayerChange(BitmapPixelChanges.FromSingleColoredArray(cords2, Colors.Transparent), 0));
+
+            var changes = controller.PopChanges();
+            Assert.Single(changes);
+            Assert.Equal(4, changes[0].Item1.PixelChanges.ChangedPixels.Count);
+            Assert.Equal(4, changes[0].Item2.PixelChanges.ChangedPixels.Count);
+        }
+
+        private static PixelChangesController CreateBasicController()
+        {
+            Coordinates[] cords = { new Coordinates(0, 0), new Coordinates(1, 1) };
+            PixelChangesController controller = new PixelChangesController();
+
+            controller.AddChanges(new LayerChange(
+                    BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Black), 0),
+                new LayerChange(BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Transparent), 0));
+            return controller;
+        }
+
+    }
+}

+ 40 - 0
PixiEditorTests/ModelsTests/ControllersTests/ReadonlyUtilityTests.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ControllersTests
+{
+    public class ReadonlyUtilityTests
+    {
+        [Fact]
+        public void TestThatExecuteToolExecutesTool()
+        {
+            bool toolUsed = false;
+
+            ReadonlyToolUtility util = new ReadonlyToolUtility();
+            util.ExecuteTool(new[]{new Coordinates(0,0)}, new TestReadonlyTool(() => toolUsed = true));
+            Assert.True(toolUsed);
+        }
+
+    }
+
+    public class TestReadonlyTool : ReadonlyTool
+    {
+        public Action ToolAction { get; set; }
+        public TestReadonlyTool(Action toolAction)
+        {
+            ToolAction = toolAction;
+        }
+
+        public override ToolType ToolType => ToolType.Select;
+        public override void Use(Coordinates[] pixels)
+        {
+            ToolAction();
+        }
+    }
+}

+ 2 - 1
PixiEditorTests/ModelsTests/ControllersTests/ShortcutControllerTests.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Text;
 using System.Windows;
 using System.Windows.Input;
@@ -13,7 +14,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
     public class ShortcutControllerTests
     {
 
-        private ShortcutController GenerateStandardShortcutController(Key shortcutKey, ModifierKeys modifiers,RelayCommand shortcutCommand)
+        private static ShortcutController GenerateStandardShortcutController(Key shortcutKey, ModifierKeys modifiers,RelayCommand shortcutCommand)
         {
             ShortcutController controller = new ShortcutController();
             controller.Shortcuts.Add(new Shortcut(shortcutKey, shortcutCommand, 0, modifiers));

+ 112 - 8
PixiEditorTests/ModelsTests/ControllersTests/UndoManagerTests.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Models.Controllers;
+using System;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using Xunit;
 
@@ -6,12 +7,14 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
 {
     public class UndoManagerTests
     {
-
         public int ExampleProperty { get; set; } = 1;
+        public TestPropertyClass TestPropClass { get; set; } = new TestPropertyClass();
 
         [Fact]
         public void TestSetRoot()
         {
+            PrepareUnoManagerForTest();
+            UndoManager.SetMainRoot(null);
             UndoManager.SetMainRoot(this);
             Assert.Equal(this, UndoManager.MainRoot);
         }
@@ -19,7 +22,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         [Fact]
         public void TestAddToUndoStack()
         {
-            PrepareUnoManagerForTests();
+            PrepareUnoManagerForTest();
             UndoManager.AddUndoChange(new Change("ExampleProperty", ExampleProperty, ExampleProperty));
             Assert.True(UndoManager.UndoStack.Count == 1);
             Assert.True((int)UndoManager.UndoStack.Peek().OldValue == ExampleProperty);
@@ -28,7 +31,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         [Fact]
         public void TestThatUndoAddsToRedoStack()
         {
-            PrepareUnoManagerForTests();
+            PrepareUnoManagerForTest();
             UndoManager.AddUndoChange(new Change("ExampleProperty", ExampleProperty, ExampleProperty));
             UndoManager.Undo();
             Assert.True(UndoManager.RedoStack.Count == 1);
@@ -37,7 +40,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         [Fact]
         public void TestUndo()
         {
-            PrepareUnoManagerForTests();
+            PrepareUnoManagerForTest();
             UndoManager.AddUndoChange(new Change("ExampleProperty", ExampleProperty, 55));
             ExampleProperty = 55;
             UndoManager.Undo();
@@ -48,7 +51,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         [Fact]
         public void TestThatRedoAddsToUndoStack()
         {
-            PrepareUnoManagerForTests();
+            PrepareUnoManagerForTest();
             UndoManager.AddUndoChange(new Change("ExampleProperty", ExampleProperty, ExampleProperty));
             UndoManager.Undo();
             UndoManager.Redo();
@@ -58,7 +61,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
         [Fact]
         public void TestRedo()
         {
-            PrepareUnoManagerForTests();
+            PrepareUnoManagerForTest();
             ExampleProperty = 55;
             UndoManager.AddUndoChange(new Change("ExampleProperty", 1, ExampleProperty));
             UndoManager.Undo();
@@ -66,12 +69,113 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
             Assert.True((int)UndoManager.UndoStack.Peek().NewValue == ExampleProperty);
         }
 
-        private void PrepareUnoManagerForTests()
+        [Fact]
+        public void TestThatUndoManagerUndoAndRedoWithCustomRootCorrectly()
+        {
+            PrepareUnoManagerForTest();
+            TestPropertyClass testProp = new TestPropertyClass();
+            int newVal = 5;
+            testProp.IntProperty = newVal;
+            UndoManager.AddUndoChange(new Change("IntProperty", 0, newVal, root: testProp));
+            Assert.Equal(newVal, testProp.IntProperty);
+            
+            UndoManager.Undo();
+
+            Assert.Equal(0, testProp.IntProperty);
+
+            UndoManager.Redo();
+
+            Assert.Equal(newVal, testProp.IntProperty);
+        }
+
+        [Fact]
+        public void TestThatMixedProcessOfUndoAndRedoWorks()
+        {
+            PrepareUnoManagerForTest();
+
+
+            int newVal = 5;
+
+
+            UndoManager.AddUndoChange(
+                new Change("ExampleProperty",
+                    ReverseProcess,
+                    new object[]{ExampleProperty},
+                    newVal));
+
+            ExampleProperty = newVal;
+
+            Assert.Equal(newVal, ExampleProperty);
+
+            UndoManager.Undo();
+
+            Assert.Equal(1, ExampleProperty);
+
+            UndoManager.Redo();
+
+            Assert.Equal(newVal, ExampleProperty);
+        }
+
+        [Fact]
+        public void TestThatProcessBasedUndoAndRedoWorks()
+        {
+            PrepareUnoManagerForTest();
+            int newVal = 5;
+            UndoManager.AddUndoChange(new Change(ReverseProcess, new object[]{ExampleProperty}, ReverseProcess, 
+                new object[]{newVal}));
+
+            ExampleProperty = newVal;
+
+            Assert.Equal(newVal, ExampleProperty);
+
+            UndoManager.Undo();
+
+            Assert.Equal(1, ExampleProperty);
+
+            UndoManager.Redo();
+
+            Assert.Equal(newVal, ExampleProperty);
+        }
+
+        [Fact]
+        public void TestThatNestedPropertyUndoWorks()
+        {
+            PrepareUnoManagerForTest();
+            int newVal = 5;
+
+            UndoManager.AddUndoChange(new Change("TestPropClass.IntProperty", TestPropClass.IntProperty, 
+                newVal));
+
+            TestPropClass.IntProperty = newVal;
+
+            Assert.Equal(newVal, TestPropClass.IntProperty);
+
+            UndoManager.Undo();
+
+            Assert.Equal(0, TestPropClass.IntProperty);
+
+            UndoManager.Redo();
+
+            Assert.Equal(newVal, TestPropClass.IntProperty);
+        }
+
+        private void ReverseProcess(object[] args)
+        {
+            ExampleProperty = (int)args[0];
+        }
+
+        private void PrepareUnoManagerForTest()
         {
             UndoManager.SetMainRoot(this);
             UndoManager.UndoStack.Clear();
             UndoManager.RedoStack.Clear();
             ExampleProperty = 1;
+            TestPropClass = new TestPropertyClass {IntProperty = 0};
         }
     }
+
+    public class TestPropertyClass
+    {
+        public int IntProperty { get; set; } = 0;
+    }
 }

+ 64 - 0
PixiEditorTests/ModelsTests/DataHoldersTests/BitmapPixelChangesTests.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows.Media;
+using PixiEditor.Exceptions;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.DataHoldersTests
+{
+    public class BitmapPixelChangesTests
+    {
+
+        [Fact]
+        public void TestThatFromSingleColoredArrayCreatesCorrectArray()
+        {
+            var color = Colors.Chocolate;
+            Coordinates[] cords = {new Coordinates(0, 0), new Coordinates(1, 0), new Coordinates(3, 2)};
+            var bmpChanges = BitmapPixelChanges.FromSingleColoredArray(cords, color);
+
+            Assert.All(bmpChanges.ChangedPixels.Values, changeColor => Assert.Equal(color, changeColor));
+            Assert.True(bmpChanges.WasBuiltAsSingleColored);
+        }
+
+        [Fact]
+        public void TestThatCombineCombineOverrideCombinesValues()
+        {
+            Coordinates[] cords1 = {new Coordinates(0, 0), new Coordinates(1, 0), new Coordinates(3, 2)};
+            Coordinates[] cords2 = {new Coordinates(3, 2), new Coordinates(0, 0), new Coordinates(5, 5)};
+            BitmapPixelChanges changes = BitmapPixelChanges.FromSingleColoredArray(cords1, Colors.Green);
+            BitmapPixelChanges changes2 = BitmapPixelChanges.FromSingleColoredArray(cords2, Colors.Red);
+
+            var output = BitmapPixelChanges.CombineOverride(new[] {changes, changes2});
+            Assert.Equal(4,output.ChangedPixels.Count);
+            Assert.Equal(Colors.Red, output.ChangedPixels[new Coordinates(3,2)]);
+            Assert.Equal(Colors.Red, output.ChangedPixels[new Coordinates(0,0)]);
+            Assert.Equal(Colors.Green, output.ChangedPixels[new Coordinates(1,0)]);
+        }
+
+        [Fact]
+        public void TestThatFromArraysThrowsError()
+        {
+            Assert.Throws<ArrayLengthMismatchException>
+                (() => BitmapPixelChanges.FromArrays(new[] {new Coordinates(0, 0)}, new[] {Colors.Red, Colors.Green}));
+        }
+
+        [Fact]
+        public void TestThatFormArraysWorks()
+        {
+            Coordinates[] coordinatesArray = {new Coordinates(0, 0), new Coordinates(2, 3), new Coordinates(5, 5)};
+            Color[] colorsArray = {Colors.Red, Colors.Green, Colors.Blue};
+            var result = BitmapPixelChanges.FromArrays(coordinatesArray, colorsArray);
+            for (int i = 0; i < coordinatesArray.Length; i++)
+            {
+                var cords = coordinatesArray[i];
+                Assert.Equal(colorsArray[i], result.ChangedPixels[cords]);
+            }
+            Assert.False(result.WasBuiltAsSingleColored);
+        }
+
+    }
+}

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

@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Windows.Media;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.Position;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.DataHoldersTests
+{
+    public class DocumentTests
+    {
+
+        [Theory]
+        [InlineData(10,10,20,20)]
+        [InlineData(1,2,5,8)]
+        [InlineData(20,20,10,10)] //TODO Anchor
+        public void TestResizeCanvasResizesProperly(int oldWidth, int oldHeight, int newWidth, int newHeight)
+        {
+            Document document = new Document(oldWidth, oldHeight);
+
+            document.ResizeCanvas(newWidth, newHeight, AnchorPoint.Top | AnchorPoint.Left);
+            Assert.Equal(newHeight, document.Height);
+            Assert.Equal(newWidth, document.Width);
+        }
+
+        [Theory]
+        [InlineData(10,10,20,20)]
+        [InlineData(5,8,10,16)]
+        public void TestResizeWorks(int oldWidth, int oldHeight, int newWidth, int newHeight)
+        {
+            Document document = new Document(oldWidth, oldHeight);
+
+            document.Resize(newWidth, newHeight);
+
+            Assert.Equal(newHeight, document.Height);
+            Assert.Equal(newWidth, document.Width);
+        }
+
+        [Theory]
+        [InlineData(10,10, 0, 0)]
+        [InlineData(50,50, 10, 49)]
+        public void TestThatClipCanvasWorksForSingleLayer(int initialWidth, int initialHeight,int additionalPixelX, int additionalPixelY)
+        {
+            Document document = new Document(initialWidth, initialHeight);
+            BitmapManager manager = new BitmapManager
+            {
+                ActiveDocument = document
+            };
+            manager.AddNewLayer("test");
+            manager.ActiveLayer.SetPixel(new Coordinates((int)Math.Ceiling(initialWidth / 2f), 
+                (int)Math.Ceiling(initialHeight / 2f)), Colors.Black);
+
+            manager.ActiveLayer.SetPixel(new Coordinates(additionalPixelX, additionalPixelY), Colors.Black);
+
+            document.ClipCanvas();
+            
+            Assert.Equal(manager.ActiveLayer.Width, document.Width);
+            Assert.Equal(manager.ActiveLayer.Height, document.Height);
+        }
+
+        [Theory]
+        [InlineData(10, 10, 0, 0)]
+        [InlineData(50, 50, 15, 23)]
+        [InlineData(3, 3, 1, 1)]
+        [InlineData(1, 1, 0, 0)]
+        public void TestThatClipCanvasWorksForMultipleLayers(int initialWidth, int initialHeight, int secondLayerPixelX, int secondLayerPixelY)
+        {
+            Document document = new Document(initialWidth, initialHeight);
+            BitmapManager manager = new BitmapManager
+            {
+                ActiveDocument = document
+            };
+            manager.AddNewLayer("test");
+            manager.ActiveLayer.SetPixel(new Coordinates((int)Math.Ceiling(initialWidth / 2f),
+                (int)Math.Ceiling(initialHeight / 2f)), Colors.Black); //Set pixel in center
+
+            manager.AddNewLayer("test2");
+
+            manager.ActiveLayer.SetPixel(new Coordinates(secondLayerPixelX, secondLayerPixelY), Colors.Black);
+
+            document.ClipCanvas();
+
+            int totalWidth = Math.Abs(manager.ActiveDocument.Layers[1].OffsetX +
+                             manager.ActiveDocument.Layers[1].Width - (manager.ActiveDocument.Layers[0].OffsetX +
+                             manager.ActiveDocument.Layers[0].Width)) + 1;
+
+            int totalHeight = Math.Abs(manager.ActiveDocument.Layers[1].OffsetY +
+                manager.ActiveDocument.Layers[1].Height - (manager.ActiveDocument.Layers[0].OffsetY +
+                                                          manager.ActiveDocument.Layers[0].Height)) + 1;
+
+            Assert.Equal(totalWidth, document.Width);
+            Assert.Equal(totalHeight, document.Height);
+        }
+
+        [Theory]
+        [InlineData(10,10)]
+        [InlineData(11,11)]
+        [InlineData(25,17)]
+        public void TestThatCenterContentCentersContentForSingleLayer(int docWidth, int docHeight)
+        {
+            Document doc = new Document(docWidth, docHeight);
+            BitmapManager manager = new BitmapManager
+            {
+                ActiveDocument = doc
+            };
+            manager.AddNewLayer("test");
+
+            manager.ActiveLayer.SetPixel(new Coordinates(0,0), Colors.Green);
+
+            doc.CenterContent();
+
+            Assert.Equal(Math.Floor(docWidth / 2f), manager.ActiveLayer.OffsetX);
+            Assert.Equal(Math.Floor(docHeight / 2f), manager.ActiveLayer.OffsetY);
+        }
+
+        [Theory]
+        [InlineData(10, 10)]
+        [InlineData(11, 11)]
+        [InlineData(25, 17)]
+        public void TestThatCenterContentCentersContentForMultipleLayers(int docWidth, int docHeight)
+        {
+            Document doc = new Document(docWidth, docHeight);
+            BitmapManager manager = new BitmapManager
+            {
+                ActiveDocument = doc
+            };
+            manager.AddNewLayer("test");
+            manager.ActiveLayer.SetPixel(new Coordinates(0, 0), Colors.Green);
+
+            manager.AddNewLayer("test2");
+            manager.ActiveLayer.SetPixel(new Coordinates(1, 1), Colors.Green);
+
+            doc.CenterContent();
+
+            int midWidth = (int)Math.Floor(docWidth / 2f);
+            int midHeight = (int)Math.Floor(docHeight / 2f);
+
+            Assert.Equal( midWidth - 1, manager.ActiveDocument.Layers[0].OffsetX);
+            Assert.Equal( midHeight - 1, manager.ActiveDocument.Layers[0].OffsetY);
+
+            Assert.Equal(midWidth, manager.ActiveDocument.Layers[1].OffsetX);
+            Assert.Equal(midHeight, manager.ActiveDocument.Layers[1].OffsetY);
+        }
+
+    }
+}

+ 57 - 0
PixiEditorTests/ModelsTests/DataHoldersTests/NotifyableColorTests.cs

@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows.Media;
+using PixiEditor.Models.DataHolders;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.DataHoldersTests
+{
+    public class NotifyableColorTests
+    {
+        [Fact]
+        public void TestThatSetArgbWorks()
+        {
+            NotifyableColor color = new NotifyableColor();
+            color.SetArgb(2,2,2,2);
+            Assert.Equal(2, color.A);
+            Assert.Equal(2, color.R);
+            Assert.Equal(2, color.G);
+            Assert.Equal(2, color.B);
+        }
+
+
+        [Theory]
+        [InlineData("A", 2)]
+        [InlineData("R", 2)]
+        [InlineData("G", 2)]
+        [InlineData("B", 2)]
+        public void TestThatPropertyChangeCalled(string prop, byte value)
+        {
+            NotifyableColor color = new NotifyableColor(Colors.Black);
+            var property = color.GetType().GetProperty(prop);
+
+            Assert.NotNull(property);
+            Assert.PropertyChanged(color, prop, () => property.SetValue(color, value));
+        }
+
+        [Theory]
+        [InlineData("A", 2)]
+        [InlineData("R", 2)]
+        [InlineData("G", 2)]
+        [InlineData("B", 2)]
+        public void TestThatEventCalled(string prop, byte value)
+        {
+            bool eventCalled = false;
+            NotifyableColor color = new NotifyableColor(Colors.Black);
+            var property = color.GetType().GetProperty(prop);
+
+            color.ColorChanged += (s,e) => eventCalled = true;
+            Assert.NotNull(property);
+
+            property.SetValue(color, value);
+
+            Assert.True(eventCalled);
+        }
+    }
+}

+ 64 - 0
PixiEditorTests/ModelsTests/DataHoldersTests/SelectionTests.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.Position;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.DataHoldersTests
+{
+    public class SelectionTests
+    {
+
+        [Fact]
+        public void TestThatSetSelectionNewSetsCorrectSelection()
+        {
+            Selection selection = new Selection(Array.Empty<Coordinates>());
+            Coordinates[] points = {new Coordinates(0, 0), new Coordinates(1, 1)};
+
+            selection.SetSelection(points, SelectionType.New);
+            selection.SetSelection(points, SelectionType.New); //Doing it twice, to check if it sets every time properly
+
+            Assert.Equal(points.Length, selection.SelectedPoints.Count);
+        }
+
+        [Fact]
+        public void TestThatSetSelectionAddSetsCorrectSelection()
+        {
+            Selection selection = new Selection(Array.Empty<Coordinates>());
+            Coordinates[] points = { new Coordinates(0, 0), new Coordinates(1, 1) };
+            Coordinates[] points2 = { new Coordinates(2, 4), new Coordinates(5, 7) };
+
+            selection.SetSelection(points, SelectionType.Add);
+            selection.SetSelection(points2, SelectionType.Add); //Doing it twice, to check if it sets every time properly
+
+            Assert.Equal(points.Length + points2.Length, selection.SelectedPoints.Count);
+        }
+
+        [Fact]
+        public void TestThatSetSelectionSubtractSetsCorrectSelection()
+        {
+            Selection selection = new Selection(Array.Empty<Coordinates>());
+            Coordinates[] points = { new Coordinates(0, 0), new Coordinates(1, 1) };
+            Coordinates[] points2 = { new Coordinates(1, 1)};
+
+            selection.SetSelection(points, SelectionType.Add);
+            selection.SetSelection(points2, SelectionType.Subtract); //Doing it twice, to check if it sets every time properly
+
+            Assert.Single(selection.SelectedPoints);
+        }
+
+        [Fact]
+        public void TestClearWorks()
+        {
+            Selection selection = new Selection(new []{new Coordinates(0,0), new Coordinates(5,7)});
+            selection.Clear();
+
+            Assert.Empty(selection.SelectedPoints);
+            Assert.Equal(0,selection.SelectionLayer.Width + selection.SelectionLayer.Height);
+        }
+
+    }
+}

+ 85 - 0
PixiEditorTests/ModelsTests/DataHoldersTests/SerializableDocumentTests.cs

@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.DataHoldersTests
+{
+    public class SerializableDocumentTests
+    {
+
+        [Fact]
+        public void TestThatSerializableDocumentCreatesCorrectly()
+        {
+            Document document = GenerateSampleDocument();
+            SerializableDocument doc = new SerializableDocument(document);
+
+            var swatch = document.Swatches.First();
+            Tuple<byte, byte, byte, byte> color = Tuple.Create(swatch.A, swatch.R, swatch.G, swatch.B);
+
+            Assert.Equal(document.Width, doc.Width);
+            Assert.Equal(document.Height, doc.Height);
+            Assert.Equal(color, doc.Swatches.First());
+            for (int i = 0; i < doc.Layers.Length; i++)
+            {
+                Assert.Equal(document.Layers[i].ConvertBitmapToBytes(), doc.Layers[i].BitmapBytes);
+                Assert.Equal(document.Layers[i].OffsetX, doc.Layers[i].OffsetX);
+                Assert.Equal(document.Layers[i].OffsetY, doc.Layers[i].OffsetY);
+                Assert.Equal(document.Layers[i].Width, doc.Layers[i].Width);
+                Assert.Equal(document.Layers[i].Height, doc.Layers[i].Height);
+                Assert.Equal(document.Layers[i].MaxWidth, doc.Layers[i].MaxWidth);
+                Assert.Equal(document.Layers[i].MaxHeight, doc.Layers[i].MaxHeight);
+                Assert.Equal(document.Layers[i].IsVisible, doc.Layers[i].IsVisible);
+                Assert.Equal(document.Layers[i].Opacity, doc.Layers[i].Opacity);
+            }
+        }
+
+        [Fact]
+        public void TestThatToDocumentConvertsCorrectly()
+        {
+            Document document = GenerateSampleDocument();
+            SerializableDocument doc = new SerializableDocument(document);
+
+            Document convertedDocument = doc.ToDocument();
+
+            Assert.Equal(document.Height, convertedDocument.Height);
+            Assert.Equal(document.Width, convertedDocument.Width);
+            Assert.Equal(document.Swatches, convertedDocument.Swatches);
+            Assert.Equal(document.Layers.Select(x=> x.LayerBitmap.ToByteArray()),
+                convertedDocument.Layers.Select(x=> x.LayerBitmap.ToByteArray()));
+        }
+
+        [Fact]
+        public void TestThatToLayersConvertsCorrectly()
+        {
+            Document document = GenerateSampleDocument();
+            SerializableDocument doc = new SerializableDocument(document);
+
+            var layers = doc.ToLayers();
+            for (int i = 0; i < layers.Count; i++)
+            {
+                Assert.Equal(document.Layers[i].LayerBitmap.ToByteArray(), layers[i].ConvertBitmapToBytes());
+                Assert.Equal(document.Layers[i].Height, layers[i].Height);
+                Assert.Equal(document.Layers[i].Width, layers[i].Width);
+                Assert.Equal(document.Layers[i].MaxHeight, layers[i].MaxHeight);
+                Assert.Equal(document.Layers[i].MaxWidth, layers[i].MaxWidth);
+                Assert.Equal(document.Layers[i].Offset, layers[i].Offset);
+                Assert.Equal(document.Layers[i].Opacity, layers[i].Opacity);
+                Assert.Equal(document.Layers[i].IsVisible, layers[i].IsVisible);
+            }
+        }
+
+        private static Document GenerateSampleDocument()
+        {
+            Document document = new Document(10, 10);
+            document.Layers.Add(new Layer("Test", 5, 8));
+            document.Swatches.Add(Colors.Green);
+            return document;
+        }
+    }
+}

+ 48 - 0
PixiEditorTests/ModelsTests/IO/BinarySerializationTests.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Windows.Media;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.Layers;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.IO
+{
+    public class BinarySerializationTests
+    {
+
+        private const string Path = "bstests.file";
+
+        [Fact]
+        public void TestThatWriteToBinaryFileCreatesFile()
+        {
+            SerializableDocument doc = new SerializableDocument(new Document(10,10));
+            BinarySerialization.WriteToBinaryFile(Path, doc);
+
+            Assert.True(File.Exists(Path));
+
+            File.Delete(Path);
+        }
+
+        [Fact]
+        public void TestThatReadFromBinaryFileReadsCorrectly()
+        {
+            Document document = new Document(10, 10);
+            document.Layers.Add(new Layer("yeet"));
+            document.Swatches.Add(Colors.Green);
+
+            SerializableDocument doc = new SerializableDocument(document);
+            BinarySerialization.WriteToBinaryFile(Path, doc);
+
+            var file = BinarySerialization.ReadFromBinaryFile<SerializableDocument>(Path);
+            
+            Assert.Equal(doc.Layers, file.Layers);
+            Assert.Equal(doc.Height, file.Height);
+            Assert.Equal(doc.Width, file.Width);
+            Assert.Equal(doc.Swatches, file.Swatches);
+        }
+
+    }
+}

+ 47 - 0
PixiEditorTests/ModelsTests/IO/ExporterTests.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.IO
+{
+    public class ExporterTests
+    {
+        private const string FilePath = "test.file";
+
+        [Fact]
+        public void TestThatSaveAsPngSavesFile()
+        {
+            Exporter.SaveAsPng(FilePath, 10, 10, BitmapFactory.New(10,10));
+            Assert.True(File.Exists(FilePath));
+
+            File.Delete(FilePath);
+        }
+
+        [Fact]
+        public void TestThatSaveAsEditableFileSavesPixiFile()
+        {
+            Document document = new Document(2,2);
+
+            string filePath = "testFile.pixi";
+
+            document.Layers.Add(new Layer("layer1"));
+            document.Layers[0].SetPixel(new Coordinates(1,1), Colors.White);
+
+            document.Swatches.Add(Colors.White);
+
+            Exporter.SaveAsEditableFile(document, filePath);
+
+            Assert.True(File.Exists(filePath));
+
+            File.Delete(filePath);
+        }
+    }
+}

+ 60 - 0
PixiEditorTests/ModelsTests/IO/ImporterTests.cs

@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.IO;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.IO
+{
+    public class ImporterTests
+    {
+        private string _testImagePath;
+
+        //I am not testing ImportDocument, because it's just a wrapper for BinarySerialization which is tested.
+
+        public ImporterTests()
+        {
+            _testImagePath = $"{Environment.CurrentDirectory}\\..\\..\\..\\ModelsTests\\IO\\TestImage.png";
+        }
+
+        [Theory]
+        [InlineData("wubba.png")]
+        [InlineData("lubba.pixi")]
+        [InlineData("dub.jpeg")]
+        [InlineData("-.JPEG")]
+        [InlineData("dub.jpg")]
+        public void TestThatIsSupportedFile(string file)
+        {
+            Assert.True(Importer.IsSupportedFile(file));
+        }
+
+        [Fact]
+        public void TestThatImportImageImportsImage()
+        {
+            var color = Color.FromArgb(255, 255, 0, 0);
+            var image = Importer.ImportImage(_testImagePath);
+
+            Assert.NotNull(image);
+            Assert.Equal(5, image.PixelWidth);
+            Assert.Equal(5, image.PixelHeight);
+            Assert.Equal(color, image.GetPixel(0,0)); //Top left
+            Assert.Equal(color, image.GetPixel(4,4)); //Bottom right
+            Assert.Equal(color, image.GetPixel(0,4)); //Bottom left
+            Assert.Equal(color, image.GetPixel(4,0)); //Top right
+            Assert.Equal(color, image.GetPixel(2,2)); //Middle center
+        }
+
+        [Fact]
+        public void TestThatImportImageResizes()
+        {
+            var image = Importer.ImportImage(_testImagePath, 10, 10);
+
+            Assert.Equal(10, image.PixelWidth);
+            Assert.Equal(10, image.PixelHeight);
+        }
+
+    }
+}

BIN
PixiEditorTests/ModelsTests/IO/TestImage.png


+ 98 - 0
PixiEditorTests/ModelsTests/ImageManipulationTests/BitmapUtilsTests.cs

@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ImageManipulationTests
+{
+    public class BitmapUtilsTests
+    {
+        [Fact]
+        public void TestBytesToWriteableBitmap()
+        {
+            int width = 10;
+            int height = 10;
+            Coordinates[] coloredPoints = {new Coordinates(0, 0), new Coordinates(3, 6), new Coordinates(9, 9)};
+            WriteableBitmap bmp = BitmapFactory.New(width, height);
+            for (int i = 0; i < coloredPoints.Length; i++)
+            {
+                bmp.SetPixel(coloredPoints[i].X, coloredPoints[i].Y, Colors.Green);
+            }
+
+            var byteArray = bmp.ToByteArray();
+
+            var convertedBitmap = BitmapUtils.BytesToWriteableBitmap(width, height, byteArray);
+
+            for (int i = 0; i < coloredPoints.Length; i++)
+            {
+                Assert.Equal(Colors.Green,convertedBitmap.GetPixel(coloredPoints[i].X, coloredPoints[i].Y));
+            }
+        }
+
+        [Fact]
+        public void TestThatCombineLayersReturnsCorrectBitmap()
+        {
+            Coordinates[] cords = {new Coordinates(0, 0), new Coordinates(1, 1)};
+            Layer[] layers = {new Layer("test", 2,2), new Layer("test2", 2, 2) };
+
+            layers[0].SetPixels(BitmapPixelChanges.FromSingleColoredArray(new []{cords[0]}, Colors.Green));
+
+            layers[1].SetPixels(BitmapPixelChanges.FromSingleColoredArray(new[] { cords[1] }, Colors.Red));
+
+            var outputBitmap = BitmapUtils.CombineLayers(layers, 2, 2);
+
+            Assert.Equal(Colors.Green, outputBitmap.GetPixel(0,0));
+            Assert.Equal(Colors.Red, outputBitmap.GetPixel(1,1));
+        }
+
+        [Fact]
+        public void TestThatCombineLayersReturnsCorrectBitmapWithSamePixels()
+        {
+            Coordinates[] cords = { new Coordinates(0, 0) };
+            Layer[] layers = { new Layer("test", 2, 2), new Layer("test2", 2, 2) };
+
+            layers[0].SetPixels(BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Green));
+
+            layers[1].SetPixels(BitmapPixelChanges.FromSingleColoredArray(cords, Colors.Red));
+
+            var outputBitmap = BitmapUtils.CombineLayers(layers, 2, 2);
+
+            Assert.Equal(Colors.Red, outputBitmap.GetPixel(0, 0));
+        }
+
+        [Fact]
+        public void TestThatGetPixelsForSelectionReturnsCorrectPixels()
+        {
+            Coordinates[] cords = { new Coordinates(0, 0),
+                new Coordinates(1,1), new Coordinates(0, 1), new Coordinates(1, 0) };
+            Layer[] layers = { new Layer("test", 2, 2), new Layer("test2", 2, 2) };
+
+            layers[0].SetPixels(BitmapPixelChanges.FromSingleColoredArray(new []{cords[0]}, Colors.Green));
+            layers[1].SetPixels(BitmapPixelChanges.FromSingleColoredArray(new [] { cords[1] }, Colors.Red));
+
+            var output = BitmapUtils.GetPixelsForSelection(layers, cords);
+
+            List<Color> colors = new List<Color>();
+
+            foreach (var layerColor in output.ToArray())
+            {
+                foreach (var color in layerColor.Value)
+                {
+                    colors.Add(color);
+                }
+            }
+            Assert.Single(colors.Where(x=> x == Colors.Green));
+            Assert.Single(colors.Where(x=> x == Colors.Red));
+            Assert.Equal(6, colors.Count(x => x.A == 0)); //6 because layer is 4 pixels,
+                                                          //2 * 4 = 8, 2 other color pixels, so 8 - 2 = 6
+        }
+
+    }
+}

+ 44 - 0
PixiEditorTests/ModelsTests/ImageManipulationTests/TransformTests.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Position;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ImageManipulationTests
+{
+    public class TransformTests
+    {
+
+        [Theory]
+        [InlineData(0,0,1,1,1,1)]
+        [InlineData(1,1,0,0,-1,-1)]
+        [InlineData(5,5,4,6,-1,1)]
+        [InlineData(-15,-15,-16,-16,-1,-1)]
+        [InlineData(150,150,1150,1150,1000,1000)]
+        public void TestGetTranslation(int x1, int y1, int x2, int y2, int expectedX, int expectedY)
+        {
+            var translation = Transform.GetTranslation(new Coordinates(x1, y1), new Coordinates(x2, y2));
+            Assert.Equal(new Coordinates(expectedX, expectedY), translation);
+        }
+
+        [Theory]
+        [InlineData(0,0)]
+        [InlineData(1,1)]
+        [InlineData(5,2)]
+        [InlineData(50,150)]
+        [InlineData(-5,-52)]
+        public void TestTranslate(int vectorX, int vectorY)
+        {
+            Coordinates[] points = {new Coordinates(0, 0), new Coordinates(5, 5), new Coordinates(15,2)};
+            var translatedCords = Transform.Translate(points, new Coordinates(vectorX, vectorY));
+
+            for (int i = 0; i < points.Length; i++)
+            {
+                Assert.Equal(points[i].X + vectorX, translatedCords[i].X);
+                Assert.Equal(points[i].Y + vectorY, translatedCords[i].Y);
+            }
+        }
+
+    }
+}

+ 144 - 0
PixiEditorTests/ModelsTests/LayersTests/LayerTests.cs

@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.LayersTests
+{
+    public class LayerTests
+    {
+        [Fact]
+        public void TestThatEmptyLayerGeneratesCorrectly()
+        {
+            Layer layer = new Layer("layer");
+
+            Assert.Equal("layer",layer.Name);
+            Assert.Equal(0,layer.Width);
+            Assert.Equal(0,layer.Height);
+            Assert.Equal(1, layer.LayerBitmap.PixelWidth);
+            Assert.Equal(1, layer.LayerBitmap.PixelHeight);
+        }
+
+        [Fact]
+        public void TestThatEmptyLayerWithSizeGeneratesCorrectly()
+        {
+            Layer layer = new Layer("layer", 10, 10);
+
+            Assert.Equal("layer", layer.Name);
+            Assert.Equal(10, layer.Width);
+            Assert.Equal(10, layer.Height);
+            Assert.Equal(10, layer.LayerBitmap.PixelWidth);
+            Assert.Equal(10, layer.LayerBitmap.PixelHeight);
+        }
+
+        [Fact]
+        public void TestThatLayerFromBitmapGeneratesCorrectly()
+        {
+            WriteableBitmap bmp = BitmapFactory.New(10, 10);
+            
+            Layer layer = new Layer("layer",bmp);
+
+            Assert.Equal("layer", layer.Name);
+            Assert.Equal(10, layer.Width);
+            Assert.Equal(10, layer.Height);
+            Assert.Equal(10, layer.LayerBitmap.PixelWidth);
+            Assert.Equal(10, layer.LayerBitmap.PixelHeight);
+        }
+
+        [Fact]
+        public void TestThatCloneClonesCorrectly()
+        {
+            Layer layer = new Layer("test", 5, 2);
+
+            var clone = layer.Clone();
+
+            Assert.Equal(layer.Name, clone.Name);
+            Assert.Equal(layer.Offset, clone.Offset);
+            Assert.Equal(layer.Width, clone.Width);
+            Assert.Equal(layer.Height, clone.Height);
+            Assert.Equal(layer.MaxHeight, clone.MaxHeight);
+            Assert.Equal(layer.MaxWidth, clone.MaxWidth);
+            Assert.Equal(layer.Opacity, clone.Opacity);
+            Assert.Equal(layer.IsVisible, clone.IsVisible);
+            Assert.Equal(layer.IsRenaming, clone.IsRenaming);
+            Assert.Equal(layer.ConvertBitmapToBytes(), clone.ConvertBitmapToBytes());
+        }
+
+        [Fact]
+        public void TestThatCloneIsMakingDeepCopyOfBitmap()
+        {
+            Layer layer = new Layer("test", 5, 2);
+
+            var clone = layer.Clone();
+
+            clone.LayerBitmap.SetPixel(0,0, Colors.Green); //Actually we are checking if modifying clone bitmap does not affect original
+
+            Assert.NotEqual(Colors.Green,layer.GetPixel(0,0));
+        }
+
+        [Fact]
+        public void TestThatResizeResizesBitmap()
+        {
+            Layer layer = new Layer("layer", 1,1);
+
+            layer.SetPixel(new Coordinates(0,0), Colors.Black);
+
+            layer.Resize(2,2, 2,2);
+
+            Assert.Equal(2,layer.Width);
+            Assert.Equal(2,layer.Height);
+            Assert.Equal(2, layer.MaxWidth);
+            Assert.Equal(2, layer.MaxHeight);
+
+            for (int y = 0; y < layer.Height; y++) //4 is new area of bitmap
+            {
+                for (int x = 0; x < layer.Width; x++)
+                {
+                    Assert.Equal(Colors.Black,layer.GetPixel(x,y));
+                }
+            }
+        }
+
+        [Fact]
+        public void TestThatGetPixelReturnsTransparentIfOutOfBounds()
+        {
+            Layer layer = new Layer("layer");
+
+            Assert.Equal(0, layer.GetPixel(-1, 999).A);
+        }
+
+        [Fact]
+        public void TestThatSetPixelsSetsPixels() //This also tests if Dynamic Resize works
+        {
+            Coordinates[] pixels = {new Coordinates(4, 2), new Coordinates(0, 0), new Coordinates(15, 2),};
+
+            Layer layer = new Layer("layer");
+
+            layer.SetPixels(BitmapPixelChanges.FromSingleColoredArray(pixels, Colors.Green));
+
+            for (int i = 0; i < pixels.Length; i++)
+            {
+                Assert.Equal(Colors.Green, layer.GetPixelWithOffset(pixels[i].X, pixels[i].Y));
+            }
+        }
+
+        [Fact]
+        public void TestThatClipCanvasResizesBitmapCorrectly()
+        {
+            Layer layer = new Layer("layer", 10, 10);
+            layer.SetPixel(new Coordinates(4,4), Colors.Blue);
+
+            layer.ClipCanvas();
+
+            Assert.Equal(1, layer.Width);
+            Assert.Equal(1, layer.Height);
+            Assert.Equal(Colors.Blue, layer.GetPixel(0,0));
+        }
+
+    }
+}

+ 2 - 2
PixiEditorTests/WorkspaceTests/ToolsTests/CoordinatesCalculatorTests.cs → PixiEditorTests/ModelsTests/PositionTests/CoordinatesCalculatorTests.cs

@@ -1,14 +1,14 @@
 using PixiEditor.Models.Position;
 using Xunit;
 
-namespace PixiEditorTests.WorkspaceTests.ToolsTests
+namespace PixiEditorTests.ModelsTests.PositionTests
 {
     public class CoordinatesCalculatorTests
     {
         [Theory]
         [InlineData(0, 0, 2, 2, 9)]
         [InlineData(0, 0, 10, 10, 121)]
-        public void RectangleToCoordinatesAmountTest(int x1, int y1, int x2, int y2, int expectedResult)
+        public void TestThatRectangleToCoordinatesReturnsSameAmount(int x1, int y1, int x2, int y2, int expectedResult)
         {
             Assert.Equal(CoordinatesCalculator.RectangleToCoordinates(x1, y1, x2, y2).Length, expectedResult);
         }

+ 30 - 0
PixiEditorTests/ModelsTests/PositionTests/CoordinatesTests.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using PixiEditor.Models.Position;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.PositionTests
+{
+    public class CoordinatesTests
+    {
+
+        [Fact]
+        public void TestThatToStringReturnsCorrectFormat()
+        {
+            Coordinates cords = new Coordinates(5,5);
+
+            Assert.Equal("5, 5",cords.ToString());
+        }
+
+        [Fact]
+        public void TestThatNotEqualOperatorWorks()
+        {
+            Coordinates cords = new Coordinates(5,5);
+            Coordinates cords2 = new Coordinates(6,4);
+
+            Assert.True(cords != cords2);
+        }
+
+    }
+}

+ 36 - 0
PixiEditorTests/ModelsTests/ToolsTests/BrightnessToolTests.cs

@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows;
+using System.Windows.Media;
+using PixiEditor;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools.Tools;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ToolsTests
+{
+    [Collection("Application collection")]
+    public class BrightnessToolTests
+    {
+        [StaTheory]
+        [InlineData(5, 12, 12, 12)]
+        [InlineData(-5, 242, 242, 242)]
+        // If correction factor is negative, testing color will be white, otherwise black
+        public void TestThatBrightnessToolChangesPixelBrightness(float correctionFactor, byte expectedR, byte expectedG, byte expectedB)
+        {
+            Color expectedColor = Color.FromRgb(expectedR, expectedG, expectedB);
+
+            BrightnessTool tool = new BrightnessTool();
+
+            Layer layer = new Layer("test", 1, 1);
+            layer.SetPixel(new Coordinates(0,0), correctionFactor < 0 ? Colors.White : Colors.Black);
+
+            var changes = tool.ChangeBrightness(layer, new Coordinates(0, 0),1,correctionFactor);
+            layer.SetPixels(changes);
+
+            Assert.Equal(expectedColor,layer.GetPixel(0,0));
+        }
+    }
+}

+ 35 - 0
PixiEditorTests/ModelsTests/ToolsTests/LineToolTests.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools.Tools;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ToolsTests
+{
+    [Collection("Application collection")]
+    public class LineToolTests
+    {
+        [StaTheory]
+        [InlineData(2)]
+        [InlineData(10)]
+        [InlineData(100)]
+        public void TestThatCreateLineCreatesDiagonalLine(int length)
+        {
+            LineTool lineTool = new LineTool();
+
+            var line = lineTool.CreateLine(new Coordinates(0,0), new Coordinates(length - 1, length - 1), 1);
+
+            Assert.Equal(length, line.Length);
+
+            for (int i = 0; i < length; i++)
+            {
+                Assert.Contains(new Coordinates(i,i), line);
+            }
+        }
+    }
+}

+ 34 - 0
PixiEditorTests/ModelsTests/ToolsTests/RectangleToolTests.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using System.Windows;
+using PixiEditor;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools.Tools;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ToolsTests
+{
+    [Collection("Application collection")]
+    public class RectangleToolTests
+    {
+        [StaTheory]
+        [InlineData(0,0, 2,2)]
+        [InlineData(0,0, 9, 9)]
+        [InlineData(5,5, 6, 6)]
+        [InlineData(0,0, 15, 15)]
+        public void TestThatCreateRectangleCalculatesCorrectOutlineWithOneThickness(int startX, int startY, int endX, int endY)
+        {
+            RectangleTool tool = new RectangleTool();
+
+            var outline = tool.CreateRectangle(new Coordinates(startX, startY),
+                new Coordinates(endX, endY), 1);
+
+            int expectedBorderPoints = (endX - startX) * 2 + (endY - startX) * 2;
+
+            Assert.Equal(expectedBorderPoints, outline.Length);
+        }
+
+    }
+}

+ 44 - 0
PixiEditorTests/ModelsTests/ToolsTests/ToolbarTests/ToolbarBaseTests.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using PixiEditor.Models.Tools.ToolSettings;
+using PixiEditor.Models.Tools.ToolSettings.Toolbars;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ToolsTests.ToolbarTests
+{
+    [Collection("Application collection")]
+    public class ToolbarBaseTests
+    {
+
+        [StaFact]
+        public void TestThatGetSettingReturnsCorrectSetting()
+        {
+            BasicToolbar toolbar = new BasicToolbar();
+            string settingName = "ToolSize";
+
+            Setting setting = toolbar.GetSetting(settingName);
+
+            Assert.NotNull(setting);
+            Assert.Equal(settingName,setting.Name);
+        }
+
+        [StaFact]
+        public void TestThatSaveToolbarSettingsSavesSettingAndLoadsItIntoNewToolbar()
+        {
+            BasicToolbar toolbar = new BasicToolbar();
+
+            toolbar.Settings[0].Value = 5;
+
+            toolbar.SaveToolbarSettings();
+
+            BasicShapeToolbar shapeToolbar = new BasicShapeToolbar();
+
+            Assert.NotEqual(5, (int)shapeToolbar.GetSetting("ToolSize").Value);
+
+            shapeToolbar.LoadSharedSettings();
+
+            Assert.Equal(5, (int)shapeToolbar.GetSetting("ToolSize").Value);
+        }
+    }
+}

+ 2 - 7
PixiEditorTests/PixiEditorTests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp3.0</TargetFramework>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
 
     <IsPackable>false</IsPackable>
   </PropertyGroup>
@@ -13,7 +13,7 @@
 
   <ItemGroup>
     <PackageReference Include="Codecov" Version="1.12.0" />
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
@@ -27,11 +27,6 @@
     <PackageReference Include="Xunit.StaFact" Version="0.3.18" />
   </ItemGroup>
 
-  <ItemGroup>
-    <Folder Include="ModelsTests\PositionTests\" />
-    <Folder Include="ViewModelsTests\" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="..\PixiEditor\PixiEditor.csproj" />
   </ItemGroup>

+ 171 - 0
PixiEditorTests/ViewModelsTests/ViewModelMainTests.cs

@@ -0,0 +1,171 @@
+using System.IO;
+using System.Windows.Media;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using PixiEditor.ViewModels;
+using Xunit;
+
+namespace PixiEditorTests.ViewModelsTests
+{
+    [Collection("Application collection")]
+    public class ViewModelMainTests
+    {
+        [StaFact]
+        public void TestThatConstructorSetsUpControllersCorrectly()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+
+            Assert.Equal(viewModel, UndoManager.MainRoot);
+            Assert.NotNull(viewModel.ChangesController);
+            Assert.NotNull(viewModel.ShortcutController);
+            Assert.NotEmpty(viewModel.ShortcutController.Shortcuts);
+            Assert.NotNull(viewModel.BitmapManager);
+            Assert.Equal(viewModel, ViewModelMain.Current);
+        }
+
+        [StaFact]
+        public void TestThatSwapColorsCommandSwapsColors()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+
+            viewModel.PrimaryColor = Colors.Black;
+            viewModel.SecondaryColor = Colors.White;
+
+            viewModel.SwapColorsCommand.Execute(null);
+
+            Assert.Equal(Colors.White, viewModel.PrimaryColor);
+            Assert.Equal(Colors.Black, viewModel.SecondaryColor);
+        }
+
+        [StaFact]
+        public void TestThatNewDocumentCreatesNewDocumentWithBaseLayer()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+
+            viewModel.NewDocument(5,5);
+
+            Assert.NotNull(viewModel.BitmapManager.ActiveDocument);
+            Assert.Single(viewModel.BitmapManager.ActiveDocument.Layers);
+        }
+
+        [StaFact]
+        public void TestThatMouseMoveCommandUpdatesCurrentCoordinates()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+
+            Assert.Equal(new Coordinates(0, 0), MousePositionConverter.CurrentCoordinates);
+
+            viewModel.MouseXOnCanvas = 5;
+            viewModel.MouseYOnCanvas = 5;
+
+            viewModel.MouseMoveCommand.Execute(null);
+
+            Assert.Equal(new Coordinates(5,5), MousePositionConverter.CurrentCoordinates);
+        }
+
+        [StaFact]
+        public void TestThatSelectToolCommandSelectsNewTool()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+
+            Assert.Equal(ToolType.Move,viewModel.BitmapManager.SelectedTool.ToolType);
+
+            viewModel.SelectToolCommand.Execute(ToolType.Line);
+
+            Assert.Equal(ToolType.Line, viewModel.BitmapManager.SelectedTool.ToolType);
+        }
+
+        [StaFact]
+        public void TestThatMouseUpCommandStopsRecordingMouseMovements()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+
+            viewModel.BitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
+
+            Assert.True(viewModel.BitmapManager.MouseController.IsRecordingChanges);
+
+            viewModel.MouseUpCommand.Execute(null);
+
+            Assert.False(viewModel.BitmapManager.MouseController.IsRecordingChanges);
+        }
+
+        [StaFact]
+        public void TestThatNewLayerCommandCreatesNewLayer()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+
+            viewModel.BitmapManager.ActiveDocument = new Document(1,1);
+
+            Assert.Empty(viewModel.BitmapManager.ActiveDocument.Layers);
+
+            viewModel.NewLayerCommand.Execute(null);
+
+            Assert.Single(viewModel.BitmapManager.ActiveDocument.Layers);
+        }
+
+        [StaFact]
+        public void TestThatSaveDocumentCommandSavesFile()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+            string fileName = "testFile.pixi";
+
+            viewModel.BitmapManager.ActiveDocument = new Document(1,1);
+
+            Exporter.SaveDocumentPath = fileName;
+
+            viewModel.SaveDocumentCommand.Execute(null);
+
+            Assert.True(File.Exists(fileName));
+
+            File.Delete(fileName);
+        }
+
+        [StaFact]
+        public void TestThatAddSwatchAddsNonDuplicateSwatch()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+            viewModel.BitmapManager.ActiveDocument = new Document(1,1);
+
+            viewModel.AddSwatch(Colors.Green);
+            viewModel.AddSwatch(Colors.Green);
+
+            Assert.Single(viewModel.BitmapManager.ActiveDocument.Swatches);
+            Assert.Equal(Colors.Green,viewModel.BitmapManager.ActiveDocument.Swatches[0]);
+        }
+
+        [StaTheory]
+        [InlineData(5,7)]
+        [InlineData(1,1)]
+        [InlineData(1,2)]
+        [InlineData(2,1)]
+        [InlineData(16,16)]
+        [InlineData(50,28)]
+        [InlineData(120,150)]
+        public void TestThatSelectAllCommandSelectsWholeDocument(int docWidth, int docHeight)
+        {
+            ViewModelMain viewModel = new ViewModelMain
+            {
+                BitmapManager = {ActiveDocument = new Document(docWidth, docHeight)}
+            };
+            viewModel.BitmapManager.AddNewLayer("layer");
+            
+            viewModel.SelectAllCommand.Execute(null);
+
+            Assert.Equal(viewModel.BitmapManager.ActiveDocument.Width * viewModel.BitmapManager.ActiveDocument.Height,
+                viewModel.ActiveSelection.SelectedPoints.Count);
+        }
+
+        [StaFact]
+        public void TestThatDocumentIsNotNullReturnsTrue()
+        {
+            ViewModelMain viewModel = new ViewModelMain();
+
+            viewModel.BitmapManager.ActiveDocument = new Document(1,1);
+
+            Assert.True(viewModel.DocumentIsNotNull(null));
+        }
+    }
+}

+ 54 - 19
README.md

@@ -1,41 +1,76 @@
-[![Build Status](https://dev.azure.com/flabbet/PixiEditor/_apis/build/status/flabbet.PixiEditor?branchName=master)](https://dev.azure.com/flabbet/PixiEditor/_build/latest?definitionId=3&branchName=master)
+[![Build Status](https://img.shields.io/azure-devops/build/flabbet/PixiEditor/3)](https://dev.azure.com/flabbet/PixiEditor/_build/latest?definitionId=3&branchName=master) 
+[![Code coverage](https://img.shields.io/azure-devops/coverage/flabbet/PixiEditor/3)](https://codecov.io/gh/flabbet/PixiEditor)
+[![Release](https://img.shields.io/github/v/release/flabbet/PixiEditor)](https://github.com/flabbet/PixiEditor/releases) 
+[![Discord Server](https://badgen.net/badge/discord/join%20chat/7289DA?icon=discord)](https://discord.gg/qSRMYmq) 
+[![contributions](https://img.shields.io/badge/contributions-open-brightgreen)](https://github.com/flabbet/PixiEditor/pulls)
 
 # PixiEditor
-PixiEditor is lightweighted pixel art creator.
 
-![screenshot](https://raw.githubusercontent.com/flabbet/PixiEditor/master/Screenshot.png)
+A Pixel art editing software. Create beautiful sprites for your games, animations (coming soon!) and edit images. All packed in eye-friendly dark theme.
 
-## Getting started with PixiEditor
+## About PixiEditor
 
-Follow these instructions to get PixiEditor working on your machine.
+Want to create beautiful pixel arts for your games? PixiEditor can help you! Our goal is to create fully open-source, fast and feature rich pixel art creator. 
 
-### Software Requirements
+### Familiar interface
+
+Have you ever used Photoshop or Gimp? Reinventing the wheel is unnecessary, we wanted experienced users to get familiar with the tool quickly and without a ease new users
+
+![](https://github.com/flabbet/PixiEditor/blob/master/Screenshot.png)
+
+
+
+### Light weighted
 
-```
-.NET Core 3.0
-```
+I takes only 157 MB to install PixiEditor.
 
-### Installing
+### Active development
 
-1. Download .zip file from [here](https://github.com/flabbet/PixiEditor/releases)
+PixiEditor started in 2018 and it's been actively developed since. We continuously improve code quality to ensure the best experience and performance.
 
-2. Open installer 
 
+
+## Installation
+
+Follow these instructions to get PixiEditor working on your machine.
+
+1. Download .exe file from [here](https://github.com/flabbet/PixiEditor/releases)
+2. Open installer
 3. Follow installer instructions
 
-## Built with
 
-* [Extended WPF Toolkit](https://github.com/xceedsoftware/wpftoolkit) - More controls and themes for WPF
-* [WriteableBitmapEx](https://github.com/teichgraf/WriteableBitmapEx/) - Collection of extension methods for the XAML WriteableBitmap
+
+## Support
+
+Struggling with something? You can find support in a few places:
+
+* Check out [documentation](https://github.com/flabbet/PixiEditor/wiki)
+
+* Ask on [Discord](https://discord.gg/XKbUBhj)
+* Open new [Issue](https://github.com/flabbet/PixiEditor/issues)
+
+
+
+## Building from source
+
+### Software Requirements
+
+* .NET Core 3.1
+
+* Visual Studio
+
+### Instructions
+
+1. Clone Repository
+
+2. Open PixiEditor/PixiEditor/PixiEditor.sln in Visual Studio
+
+3. Build solution
 
 ## Contributing 
 
 Please read [CONTRIBUTING.md](https://github.com/flabbet/PixiEditor/blob/master/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
 
-## Authors
-* **Krzysztof Krysiński** *Creator of the project* - [flabbet](https://github.com/flabbet)
-* [Contributors](https://github.com/flabbet/PixiEditor/graphs/contributors)
-
 ## License
 
 This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/flabbet/PixiEditor/blob/master/LICENSE) - file for details

BIN
Screenshot.png


+ 1 - 1
azure-pipelines.yml

@@ -35,7 +35,7 @@ steps:
   inputs:
     command: test
     projects: '**/*Tests/*.csproj'
-    arguments: '--configuration $(buildConfiguration)'
+    arguments: '--configuration $(buildConfiguration) --collect "Code coverage"'
 
 - task: PowerShell@2
   inputs:

BIN
icon.ico


BIN
screenshot.png