Просмотр исходного кода

Merge pull request #172 from PixiEditor/group-layers

Group layers
Krzysztof Krysiński 4 лет назад
Родитель
Сommit
1bec1c205d
100 измененных файлов с 4882 добавлено и 1023 удалено
  1. 81 75
      Custom.ruleset
  2. 2 0
      PixiEditor/App.xaml
  3. 2 0
      PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs
  4. 47 0
      PixiEditor/Helpers/Converters/FinalIsVisibleToVisiblityConverter.cs
  5. 48 0
      PixiEditor/Helpers/Converters/FormattedColorConverter.cs
  6. 10 3
      PixiEditor/Helpers/Converters/IndentConverter.cs
  7. 27 0
      PixiEditor/Helpers/Converters/IndexOfConverter.cs
  8. 24 0
      PixiEditor/Helpers/Converters/InverseBooleanConverter.cs
  9. 56 0
      PixiEditor/Helpers/Converters/LayerStructureToGroupsConverter.cs
  10. 40 0
      PixiEditor/Helpers/Converters/LayerToFinalOpacityConverter.cs
  11. 90 0
      PixiEditor/Helpers/Converters/LayersToStructuredLayersConverter.cs
  12. 26 0
      PixiEditor/Helpers/Converters/ZoomToViewportConverter.cs
  13. 66 5
      PixiEditor/Helpers/Extensions/ParserHelpers.cs
  14. 26 0
      PixiEditor/Helpers/UI/TreeViewItemHelper.cs
  15. BIN
      PixiEditor/Images/CheckerTile.png
  16. BIN
      PixiEditor/Images/DiagonalRed.png
  17. BIN
      PixiEditor/Images/Folder-add.png
  18. BIN
      PixiEditor/Images/Folder.png
  19. BIN
      PixiEditor/Images/Layer-add.png
  20. BIN
      PixiEditor/Images/Trash.png
  21. 3 3
      PixiEditor/Models/Controllers/BitmapManager.cs
  22. 48 8
      PixiEditor/Models/Controllers/BitmapOperationsUtility.cs
  23. 132 131
      PixiEditor/Models/Controllers/ClipboardController.cs
  24. 1 1
      PixiEditor/Models/Controllers/MouseMovementController.cs
  25. 3 2
      PixiEditor/Models/Controllers/ReadonlyToolUtility.cs
  26. 59 0
      PixiEditor/Models/Controllers/UndoManager.cs
  27. 30 0
      PixiEditor/Models/DataHolders/Document/Document.Constructors.cs
  28. 308 44
      PixiEditor/Models/DataHolders/Document/Document.Layers.cs
  29. 6 10
      PixiEditor/Models/DataHolders/Document/Document.Operations.cs
  30. 9 4
      PixiEditor/Models/DataHolders/Document/Document.cs
  31. 0 26
      PixiEditor/Models/IO/BinarySerialization.cs
  32. 1 2
      PixiEditor/Models/IO/Exporter.cs
  33. 57 57
      PixiEditor/Models/IO/Importer.cs
  34. 17 2
      PixiEditor/Models/ImageManipulation/BitmapUtils.cs
  35. 15 0
      PixiEditor/Models/Layers/GroupChangedEventArgs.cs
  36. 23 0
      PixiEditor/Models/Layers/GroupData.cs
  37. 164 0
      PixiEditor/Models/Layers/GuidStructureItem.cs
  38. 12 2
      PixiEditor/Models/Layers/Layer.cs
  39. 123 0
      PixiEditor/Models/Layers/LayerGroup.cs
  40. 1 1
      PixiEditor/Models/Layers/LayerHelper.cs
  41. 724 0
      PixiEditor/Models/Layers/LayerStructure.cs
  42. 20 0
      PixiEditor/Models/Layers/LayerStructureChangedEventArgs.cs
  43. 203 0
      PixiEditor/Models/Layers/StructuredLayerTree.cs
  44. 36 0
      PixiEditor/Models/Layers/Utils/LayerStructureUtils.cs
  45. 7 10
      PixiEditor/Models/Position/CoordinatesCalculator.cs
  46. 3 1
      PixiEditor/Models/Tools/BitmapOperationTool.cs
  47. 3 2
      PixiEditor/Models/Tools/ReadonlyTool.cs
  48. 43 43
      PixiEditor/Models/Tools/ShapeTool.cs
  49. 1 1
      PixiEditor/Models/Tools/Tools/BrightnessTool.cs
  50. 1 1
      PixiEditor/Models/Tools/Tools/CircleTool.cs
  51. 3 2
      PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
  52. 5 4
      PixiEditor/Models/Tools/Tools/EraserTool.cs
  53. 1 1
      PixiEditor/Models/Tools/Tools/FloodFill.cs
  54. 3 3
      PixiEditor/Models/Tools/Tools/LineTool.cs
  55. 7 7
      PixiEditor/Models/Tools/Tools/MoveTool.cs
  56. 3 2
      PixiEditor/Models/Tools/Tools/MoveViewportTool.cs
  57. 3 2
      PixiEditor/Models/Tools/Tools/PenTool.cs
  58. 3 3
      PixiEditor/Models/Tools/Tools/RectangleTool.cs
  59. 2 2
      PixiEditor/Models/Tools/Tools/SelectTool.cs
  60. 2 1
      PixiEditor/Models/Tools/Tools/ZoomTool.cs
  61. 6 8
      PixiEditor/Models/Undo/StorageBasedChange.cs
  62. 18 3
      PixiEditor/PixiEditor.csproj
  63. 28 29
      PixiEditor/Styles/DarkScrollBarStyle.xaml
  64. 28 0
      PixiEditor/Styles/ListSwitchButtonStyle.xaml
  65. 129 0
      PixiEditor/Styles/TreeViewStyle.xaml
  66. 0 1
      PixiEditor/ViewModels/SaveFilePopupViewModel.cs
  67. 5 4
      PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs
  68. 4 2
      PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs
  69. 0 2
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  70. 197 21
      PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  71. 22 17
      PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs
  72. 10 11
      PixiEditor/ViewModels/ViewModelMain.cs
  73. 1 2
      PixiEditor/Views/Dialogs/HelloTherePopup.xaml.cs
  74. 0 1
      PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml.cs
  75. 34 103
      PixiEditor/Views/MainWindow.xaml
  76. 180 180
      PixiEditor/Views/MainWindow.xaml.cs
  77. 52 35
      PixiEditor/Views/UserControls/DrawingViewPort.xaml
  78. 3 1
      PixiEditor/Views/UserControls/DrawingViewPort.xaml.cs
  79. 67 0
      PixiEditor/Views/UserControls/LayerGroupControl.xaml
  80. 312 0
      PixiEditor/Views/UserControls/LayerGroupControl.xaml.cs
  81. 28 23
      PixiEditor/Views/UserControls/LayerItem.xaml
  82. 92 17
      PixiEditor/Views/UserControls/LayerItem.xaml.cs
  83. 56 0
      PixiEditor/Views/UserControls/LayerStructureItemContainer.xaml
  84. 48 0
      PixiEditor/Views/UserControls/LayerStructureItemContainer.xaml.cs
  85. 93 0
      PixiEditor/Views/UserControls/LayersManager.xaml
  86. 284 0
      PixiEditor/Views/UserControls/LayersManager.xaml.cs
  87. 72 0
      PixiEditor/Views/UserControls/ListSwitchButton.cs
  88. 61 88
      PixiEditor/Views/UserControls/PreviewWindow.xaml
  89. 1 1
      PixiEditor/Views/UserControls/PreviewWindow.xaml.cs
  90. 76 0
      PixiEditor/Views/UserControls/RawLayersViewer.xaml
  91. 44 0
      PixiEditor/Views/UserControls/RawLayersViewer.xaml.cs
  92. 1 1
      PixiEditor/Views/UserControls/SwatchesView.xaml
  93. 24 0
      PixiEditor/Views/UserControls/SwitchItem.cs
  94. 11 0
      PixiEditor/Views/UserControls/SwitchItemObservableCollection.xaml.cs
  95. 3 2
      PixiEditorTests/ModelsTests/ControllersTests/MockedSinglePixelPenTool.cs
  96. 2 1
      PixiEditorTests/ModelsTests/ControllersTests/ReadonlyUtilityTests.cs
  97. 2 1
      PixiEditorTests/ModelsTests/ControllersTests/TestReadonlyTool.cs
  98. 13 6
      PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs
  99. 243 0
      PixiEditorTests/ModelsTests/DataHoldersTests/LayerStructureTests.cs
  100. 2 2
      PixiEditorTests/PixiEditorTests.csproj

+ 81 - 75
Custom.ruleset

@@ -1,76 +1,82 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RuleSet Name="Name" Description="Description" ToolsVersion="16.0">
-  <Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
-    <Rule Id="IDE0090" Action="None" />
-  </Rules>
-  <Rules AnalyzerId="Microsoft.NetCore.Analyzers" RuleNamespace="Microsoft.NetCore.Analyzers">
-    <Rule Id="CA1303" Action="None" />
-  </Rules>
-  <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
-    <Rule Id="SA0001" Action="None" />
-    <Rule Id="SA1000" Action="None" />
-    <Rule Id="SA1005" Action="None" />
-    <Rule Id="SA1007" Action="None" />
-    <Rule Id="SA1028" Action="None" />
-    <Rule Id="SA1101" Action="None" />
-    <Rule Id="SA1110" Action="None" />
-    <Rule Id="SA1111" Action="None" />
-    <Rule Id="SA1112" Action="None" />
-    <Rule Id="SA1117" Action="None" />
-    <Rule Id="SA1119" Action="None" />
-    <Rule Id="SA1122" Action="None" />
-    <Rule Id="SA1124" Action="None" />
-    <Rule Id="SA1128" Action="None" />
-    <Rule Id="SA1130" Action="None" />
-    <Rule Id="SA1132" Action="None" />
-    <Rule Id="SA1135" Action="None" />
-    <Rule Id="SA1136" Action="None" />
-    <Rule Id="SA1139" Action="None" />
-    <Rule Id="SA1200" Action="None" />
-    <Rule Id="SA1201" Action="None" />
-    <Rule Id="SA1207" Action="None" />
-    <Rule Id="SA1208" Action="None" />
-    <Rule Id="SA1209" Action="None" />
-    <Rule Id="SA1210" Action="None" />
-    <Rule Id="SA1211" Action="None" />
-    <Rule Id="SA1216" Action="None" />
-    <Rule Id="SA1217" Action="None" />
-    <Rule Id="SA1303" Action="None" />
-    <Rule Id="SA1304" Action="None" />
-    <Rule Id="SA1307" Action="None" />
-    <Rule Id="SA1309" Action="None" />
-    <Rule Id="SA1310" Action="None" />
-    <Rule Id="SA1311" Action="None" />
-    <Rule Id="SA1400" Action="None" />
-    <Rule Id="SA1401" Action="None" />
-    <Rule Id="SA1405" Action="None" />
-    <Rule Id="SA1406" Action="None" />
-    <Rule Id="SA1407" Action="None" />
-    <Rule Id="SA1410" Action="None" />
-    <Rule Id="SA1411" Action="None" />
-    <Rule Id="SA1413" Action="None" />
-    <Rule Id="SA1501" Action="None" />
-    <Rule Id="SA1502" Action="None" />
-    <Rule Id="SA1503" Action="None" />
-    <Rule Id="SA1505" Action="None" />
-    <Rule Id="SA1507" Action="None" />
-    <Rule Id="SA1508" Action="None" />
-    <Rule Id="SA1512" Action="None" />
-    <Rule Id="SA1513" Action="None" />
-    <Rule Id="SA1515" Action="None" />
-    <Rule Id="SA1516" Action="None" />
-    <Rule Id="SA1518" Action="None" />
-    <Rule Id="SA1600" Action="None" />
-    <Rule Id="SA1601" Action="None" />
-    <Rule Id="SA1602" Action="None" />
-    <Rule Id="SA1604" Action="None" />
-    <Rule Id="SA1605" Action="None" />
-    <Rule Id="SA1606" Action="None" />
-    <Rule Id="SA1607" Action="None" />
-    <Rule Id="SA1629" Action="None" />
-    <Rule Id="SA1633" Action="None" />
-    <Rule Id="SA1642" Action="None" />
-    <Rule Id="SA1643" Action="None" />
-    <Rule Id="SA1648" Action="None" />
-  </Rules>
+<?xml version="1.0" encoding="utf-8"?>
+<RuleSet Name="Name" Description="Description" ToolsVersion="16.0">
+  <Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">
+    <Rule Id="AD0001" Action="None" />
+  </Rules>
+  <Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
+    <Rule Id="IDE0090" Action="None" />
+  </Rules>
+  <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.CodeAnalysis.NetAnalyzers">
+    <Rule Id="CA1416" Action="None" />
+  </Rules>
+  <Rules AnalyzerId="Microsoft.NetCore.Analyzers" RuleNamespace="Microsoft.NetCore.Analyzers">
+    <Rule Id="CA1303" Action="None" />
+    <Rule Id="CA1416" Action="None" />
+  </Rules>
+  <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
+    <Rule Id="SA0001" Action="None" />
+    <Rule Id="SA1000" Action="None" />
+    <Rule Id="SA1008" Action="None" />
+    <Rule Id="SA1009" Action="None" />
+    <Rule Id="SA1101" Action="None" />
+    <Rule Id="SA1110" Action="None" />
+    <Rule Id="SA1111" Action="None" />
+    <Rule Id="SA1112" Action="None" />
+    <Rule Id="SA1117" Action="None" />
+    <Rule Id="SA1119" Action="None" />
+    <Rule Id="SA1122" Action="None" />
+    <Rule Id="SA1124" Action="None" />
+    <Rule Id="SA1128" Action="None" />
+    <Rule Id="SA1130" Action="None" />
+    <Rule Id="SA1132" Action="None" />
+    <Rule Id="SA1135" Action="None" />
+    <Rule Id="SA1136" Action="None" />
+    <Rule Id="SA1139" Action="None" />
+    <Rule Id="SA1200" Action="None" />
+    <Rule Id="SA1201" Action="None" />
+    <Rule Id="SA1207" Action="None" />
+    <Rule Id="SA1208" Action="None" />
+    <Rule Id="SA1209" Action="None" />
+    <Rule Id="SA1210" Action="None" />
+    <Rule Id="SA1211" Action="None" />
+    <Rule Id="SA1216" Action="None" />
+    <Rule Id="SA1217" Action="None" />
+    <Rule Id="SA1303" Action="None" />
+    <Rule Id="SA1304" Action="None" />
+    <Rule Id="SA1307" Action="None" />
+    <Rule Id="SA1309" Action="None" />
+    <Rule Id="SA1310" Action="None" />
+    <Rule Id="SA1311" Action="None" />
+    <Rule Id="SA1400" Action="None" />
+    <Rule Id="SA1401" Action="None" />
+    <Rule Id="SA1405" Action="None" />
+    <Rule Id="SA1406" Action="None" />
+    <Rule Id="SA1407" Action="None" />
+    <Rule Id="SA1410" Action="None" />
+    <Rule Id="SA1411" Action="None" />
+    <Rule Id="SA1413" Action="None" />
+    <Rule Id="SA1501" Action="None" />
+    <Rule Id="SA1502" Action="None" />
+    <Rule Id="SA1503" Action="None" />
+    <Rule Id="SA1505" Action="None" />
+    <Rule Id="SA1507" Action="None" />
+    <Rule Id="SA1508" Action="None" />
+    <Rule Id="SA1512" Action="None" />
+    <Rule Id="SA1513" Action="None" />
+    <Rule Id="SA1515" Action="None" />
+    <Rule Id="SA1516" Action="None" />
+    <Rule Id="SA1518" Action="None" />
+    <Rule Id="SA1600" Action="None" />
+    <Rule Id="SA1601" Action="None" />
+    <Rule Id="SA1602" Action="None" />
+    <Rule Id="SA1604" Action="None" />
+    <Rule Id="SA1605" Action="None" />
+    <Rule Id="SA1606" Action="None" />
+    <Rule Id="SA1607" Action="None" />
+    <Rule Id="SA1629" Action="None" />
+    <Rule Id="SA1633" Action="None" />
+    <Rule Id="SA1642" Action="None" />
+    <Rule Id="SA1643" Action="None" />
+    <Rule Id="SA1648" Action="None" />
+  </Rules>
 </RuleSet>

+ 2 - 0
PixiEditor/App.xaml

@@ -15,6 +15,7 @@
                 <ResourceDictionary Source="Styles/DarkScrollBarStyle.xaml" />
                 <ResourceDictionary Source="Styles/ImageCheckBoxStyle.xaml" />
                 <ResourceDictionary Source="Styles/DarkCheckboxStyle.xaml" />
+                <ResourceDictionary Source="Styles/ListSwitchButtonStyle.xaml" />
                 <ResourceDictionary Source="Styles/LabelStyles.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/DarkBrushes.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Menu/DarkBrushes.xaml" />
@@ -23,6 +24,7 @@
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Icons/IconGeometry.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/Themes/Generic.xaml" />
                 <ResourceDictionary Source="Styles/AvalonDock/PixiEditorDockTheme.xaml" />
+                <ResourceDictionary Source="Styles/TreeViewStyle.xaml" />
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Application.Resources>

+ 2 - 0
PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs

@@ -1,5 +1,6 @@
 using System.Windows;
 using System.Windows.Interactivity;
+using PixiEditor.Models.Controllers.Shortcuts;
 
 namespace PixiEditor.Helpers.Behaviours
 {
@@ -19,6 +20,7 @@ namespace PixiEditor.Helpers.Behaviours
         private void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
         {
             AssociatedObject.Focus();
+            ShortcutController.BlockShortcutExecution = false;
         }
     }
 }

+ 47 - 0
PixiEditor/Helpers/Converters/FinalIsVisibleToVisiblityConverter.cs

@@ -0,0 +1,47 @@
+using PixiEditor.Models.Layers;
+using PixiEditor.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Markup;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class FinalIsVisibleToVisiblityConverter : MarkupExtension, IMultiValueConverter
+    {
+        private static FinalIsVisibleToVisiblityConverter converter = null;
+
+        public override object ProvideValue(IServiceProvider serviceProvider)
+        {
+            if (converter == null)
+            {
+                converter = new FinalIsVisibleToVisiblityConverter();
+            }
+
+            return converter;
+        }
+
+        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (values[0] is Layer layer)
+            {
+                if (ViewModelMain.Current?.BitmapManager?.ActiveDocument != null)
+                {
+                    return ViewModelMain.Current.BitmapManager.ActiveDocument.GetFinalLayerIsVisible(layer) ? Visibility.Visible : Visibility.Collapsed;
+                }
+            }
+
+            return Visibility.Visible;
+        }
+
+        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 48 - 0
PixiEditor/Helpers/Converters/FormattedColorConverter.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Data;
+using System.Windows.Markup;
+using System.Windows.Media;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class FormattedColorConverter : MarkupExtension, IMultiValueConverter
+    {
+        private static FormattedColorConverter converter;
+        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+        {
+            if(values != null && values.Length > 1 && values[0] is Color color && values[1] is string format)
+            {
+                switch (format.ToLower())
+                {
+                    case "hex":
+                        return color.ToString();
+                    case "rgba":
+                        return $"({color.R}, {color.G}, {color.B}, {color.A})";
+                    default:
+                        break;
+                }
+            }
+
+            return "";
+        }
+
+        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+
+        public override object ProvideValue(IServiceProvider serviceProvider)
+        {
+            if(converter == null)
+            {
+                converter = new FormattedColorConverter();
+            }
+            return converter;
+        }
+    }
+}

+ 10 - 3
PixiEditor/Helpers/Converters/FloatNormalizeConverter.cs → PixiEditor/Helpers/Converters/IndentConverter.cs

@@ -1,19 +1,26 @@
 using System;
+using System.Collections.Generic;
 using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
 using System.Windows.Data;
 
 namespace PixiEditor.Helpers.Converters
 {
-    public class FloatNormalizeConverter : IValueConverter
+    public class IndentConverter : IValueConverter
     {
+        private const int IndentSize = 20;
+
         public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
-            return (float)value * 100;
+            return new GridLength(((GridLength)value).Value + IndentSize);
         }
 
         public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
         {
-            return (float)value / 100;
+            return Binding.DoNothing;
         }
     }
 }

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

@@ -0,0 +1,27 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Data;
+using PixiEditor.Models.Layers;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class IndexOfConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value is Layer layer && ViewModelMain.Current.BitmapManager.ActiveDocument != null)
+            {
+                return ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.IndexOf(layer);
+            }
+
+            return Binding.DoNothing;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 24 - 0
PixiEditor/Helpers/Converters/InverseBooleanConverter.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Windows.Data;
+
+namespace PixiEditor.Helpers.Converters
+{
+    [ValueConversion(typeof(bool), typeof(bool))]
+    public class InverseBooleanConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+        {
+            if (targetType != typeof(bool))
+            {
+                throw new InvalidOperationException("The target must be a boolean");
+            }
+
+            return !(bool)value;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+        {
+            throw new NotSupportedException();
+        }
+    }
+}

+ 56 - 0
PixiEditor/Helpers/Converters/LayerStructureToGroupsConverter.cs

@@ -0,0 +1,56 @@
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Layers;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Data;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class LayerStructureToGroupsConverter : IMultiValueConverter
+    {
+        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (values[0] is LayerStructure structure)
+            {
+                return GetSubGroups(structure.Groups);
+            }
+
+            return Binding.DoNothing;
+        }
+
+        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+
+        private ObservableCollection<GuidStructureItem> GetSubGroups(IEnumerable<GuidStructureItem> groups)
+        {
+            ObservableCollection<GuidStructureItem> finalGroups = new ObservableCollection<GuidStructureItem>();
+            foreach (var group in groups)
+            {
+                finalGroups.AddRange(GetSubGroups(group));
+            }
+
+            return finalGroups;
+        }
+
+        private IEnumerable<GuidStructureItem> GetSubGroups(GuidStructureItem group)
+        {
+            List<GuidStructureItem> groups = new List<GuidStructureItem>() { group };
+
+            foreach (var subGroup in group.Subgroups)
+            {
+                groups.Add(subGroup);
+                if (subGroup.Subgroups.Count > 0)
+                {
+                    groups.AddRange(GetSubGroups(subGroup));
+                }
+            }
+
+            return groups.Distinct();
+        }
+    }
+}

+ 40 - 0
PixiEditor/Helpers/Converters/LayerToFinalOpacityConverter.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using System.Windows.Markup;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Layers.Utils;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class LayerToFinalOpacityConverter : MarkupExtension, IMultiValueConverter
+    {
+        private static LayerToFinalOpacityConverter converter;
+
+        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (values.Length > 0 && values[0] is Layer layer && ViewModelMain.Current?.BitmapManager?.ActiveDocument != null)
+            {
+                return (double)LayerStructureUtils.GetFinalLayerOpacity(layer, ViewModelMain.Current.BitmapManager.ActiveDocument.LayerStructure);
+            }
+
+            return null;
+        }
+
+        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        {
+            return null;
+        }
+
+        public override object ProvideValue(IServiceProvider serviceProvider)
+        {
+            if(converter == null)
+            {
+                converter = new LayerToFinalOpacityConverter();
+            }
+
+            return converter;
+        }
+    }
+}

+ 90 - 0
PixiEditor/Helpers/Converters/LayersToStructuredLayersConverter.cs

@@ -0,0 +1,90 @@
+using PixiEditor.Models.Layers;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Data;
+
+namespace PixiEditor.Helpers.Converters
+{
+    // TODO: Implement rebuilding only changed items instead whole tree
+    public class LayersToStructuredLayersConverter : IMultiValueConverter
+    {
+        private static StructuredLayerTree cachedTree;
+        private List<Guid> lastLayers = new List<Guid>();
+        private ObservableCollection<GuidStructureItem> lastStructure = new ObservableCollection<GuidStructureItem>();
+
+        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (values[0] is ObservableCollection<Layer> layers && values[1] is LayerStructure structure)
+            {
+                if (cachedTree == null)
+                {
+                    cachedTree = new StructuredLayerTree(layers, structure);
+                }
+
+                if (TryFindStructureDifferences(structure) || lastLayers.Count != layers.Count || LayerOrderIsDifferent(layers))
+                {
+                    cachedTree = new StructuredLayerTree(layers, structure);
+
+                    lastLayers = layers.Select(x => x.LayerGuid).ToList();
+                    lastStructure = structure.CloneGroups();
+                }
+
+                return cachedTree.RootDirectoryItems;
+            }
+
+            return new StructuredLayerTree(null, null);
+        }
+        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        {
+            throw new ArgumentException("Value is not a StructuredLayerTree");
+        }
+        private bool LayerOrderIsDifferent(IList<Layer> layers)
+        {
+            var guids = layers.Select(x => x.LayerGuid).ToArray();
+            return !guids.SequenceEqual(lastLayers);
+        }
+        private bool TryFindStructureDifferences(LayerStructure structure)
+        {
+            bool structureModified = false;
+
+            if (lastStructure.Count != structure.Groups.Count)
+            {
+                return true;
+            }
+
+
+            foreach (GuidStructureItem treeItem in lastStructure)
+            {
+                var matchingGroup = structure.Groups.FirstOrDefault(x => x.GroupGuid == treeItem.GroupGuid);
+                List<GuidStructureItem> changedGroups = new List<GuidStructureItem>();
+                if (matchingGroup == null || StructureMismatch(treeItem, matchingGroup))
+                {
+                    structureModified = true;
+                }
+
+            }
+
+            return structureModified;
+        }
+
+        private bool StructureMismatch(GuidStructureItem first, GuidStructureItem second)
+        {
+            bool rootMismatch = first.EndLayerGuid != second.EndLayerGuid || first.StartLayerGuid != second.StartLayerGuid || first.IsVisible != second.IsVisible || first.IsExpanded != second.IsExpanded || first.Opacity != second.Opacity || first.Subgroups.Count != second.Subgroups.Count || second.Name != first.Name;
+
+            if (!rootMismatch && first.Subgroups.Count > 0)
+            {
+                for (int i = 0; i < first.Subgroups.Count; i++)
+                {
+                    if (StructureMismatch(first.Subgroups[i], second.Subgroups[i]))
+                    {
+                        return true;
+                    }
+                }
+            }
+            return rootMismatch;
+        }
+    }
+}

+ 26 - 0
PixiEditor/Helpers/Converters/ZoomToViewportConverter.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class ZoomToViewportConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if(value is double scale)
+            {
+                double newSize = Math.Clamp((double)parameter / scale, 1, 9999);
+                return new Rect(0, 0, newSize, newSize);
+            }
+
+            return Binding.DoNothing;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 66 - 5
PixiEditor/Helpers/Extensions/ParserHelpers.cs

@@ -1,12 +1,15 @@
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
+using PixiEditor.Parser;
+using PixiEditor.Parser.Models;
+using System;
 using System.Collections.ObjectModel;
 using System.Linq;
 using System.Windows;
 using System.Windows.Media;
 using SDColor = System.Drawing.Color;
-
+
 namespace PixiEditor.Helpers.Extensions
 {
     public static class ParserHelpers
@@ -20,6 +23,8 @@ namespace PixiEditor.Helpers.Extensions
                     Color.FromArgb(x.A, x.R, x.G, x.B)))
             };
 
+            document.LayerStructure.Groups = serializableDocument.ToGroups();
+
             if (document.Layers.Count > 0)
             {
                 document.SetMainActiveLayer(0);
@@ -28,7 +33,12 @@ namespace PixiEditor.Helpers.Extensions
             return document;
         }
 
-        public static ObservableCollection<Layer> ToLayers(this Parser.SerializableDocument serializableDocument)
+        public static ObservableCollection<GuidStructureItem> ToGroups(this SerializableDocument serializableDocument)
+        {
+            return ToGroups(serializableDocument.Groups);
+        }
+
+        public static ObservableCollection<Layer> ToLayers(this SerializableDocument serializableDocument)
         {
             ObservableCollection<Layer> layers = new ObservableCollection<Layer>();
             for (int i = 0; i < serializableDocument.Layers.Count; i++)
@@ -42,30 +52,48 @@ namespace PixiEditor.Helpers.Extensions
                         Opacity = serLayer.Opacity,
                         MaxHeight = serializableDocument.Height,
                         MaxWidth = serializableDocument.Width,
-                    };
+                    };
+                if (serLayer.LayerGuid != Guid.Empty)
+                {
+                    layer.ChangeGuid(serLayer.LayerGuid);
+                }
                 layers.Add(layer);
             }
 
             return layers;
         }
 
-        public static Parser.SerializableDocument ToSerializable(this Document document)
+        public static SerializableDocument ToSerializable(this Document document)
         {
             Parser.SerializableDocument serializable = new Parser.SerializableDocument
             {
                 Width = document.Width,
                 Height = document.Height,
                 Layers = document.Layers.Select(x => x.ToSerializable()).ToList(),
+                Groups = document.LayerStructure.Groups.Select(x => x.ToSerializable()).ToArray(),
                 Swatches = document.Swatches.Select(x => SDColor.FromArgb(x.A, x.R, x.G, x.B)).ToList()
             };
 
             return serializable;
         }
 
+        public static SerializableGuidStructureItem ToSerializable(this GuidStructureItem group, SerializableGuidStructureItem parent = null)
+        {
+            var serializedGroup = new SerializableGuidStructureItem(
+                    group.GroupGuid,
+                    group.Name,
+                    group.StartLayerGuid,
+                    group.EndLayerGuid,
+                    null, group.IsVisible, group.Opacity);
+            serializedGroup.Subgroups = group.Subgroups.Select(x => x.ToSerializable(serializedGroup)).ToArray();
+            return serializedGroup;
+        }
+
         public static Parser.SerializableLayer ToSerializable(this Layer layer)
         {
             Parser.SerializableLayer serializable = new Parser.SerializableLayer
             {
+                LayerGuid = layer.LayerGuid,
                 Name = layer.Name,
                 Width = layer.Width,
                 Height = layer.Height,
@@ -79,6 +107,39 @@ namespace PixiEditor.Helpers.Extensions
             };
 
             return serializable;
+        }
+
+        private static ObservableCollection<GuidStructureItem> ToGroups(SerializableGuidStructureItem[] serializableGroups, GuidStructureItem parent = null)
+        {
+            ObservableCollection<GuidStructureItem> groups = new ObservableCollection<GuidStructureItem>();
+
+            if (serializableGroups == null)
+            {
+                return groups;
+            }
+
+            foreach (var serializableGroup in serializableGroups)
+            {
+                groups.Add(ToGroup(serializableGroup, parent));
+            }
+            return groups;
+        }
+
+        private static GuidStructureItem ToGroup(SerializableGuidStructureItem group, GuidStructureItem parent = null)
+        {
+            if (group == null)
+            {
+                return null;
+            }
+            var parsedGroup = new GuidStructureItem(
+                group.Name,
+                group.StartLayerGuid,
+                group.EndLayerGuid,
+                new ObservableCollection<GuidStructureItem>(),
+                parent)
+            { Opacity = group.Opacity, IsVisible = group.IsVisible, GroupGuid = group.GroupGuid, IsExpanded = true };
+            parsedGroup.Subgroups = ToGroups(group.Subgroups, parsedGroup);
+            return parsedGroup;
         }
     }
-}
+}

+ 26 - 0
PixiEditor/Helpers/UI/TreeViewItemHelper.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PixiEditor.Helpers.UI
+{
+    public static class TreeViewItemHelper
+    {
+        public static GridLength GetIndent(DependencyObject obj)
+        {
+            return (GridLength)obj.GetValue(IndentProperty);
+        }
+
+        public static void SetIndent(DependencyObject obj, GridLength value)
+        {
+            obj.SetValue(IndentProperty, value);
+        }
+
+        // Using a DependencyProperty as the backing store for Indent.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty IndentProperty =
+            DependencyProperty.RegisterAttached("Indent", typeof(GridLength), typeof(TreeViewItemHelper), new PropertyMetadata(new GridLength(0)));
+    }
+}

BIN
PixiEditor/Images/CheckerTile.png


BIN
PixiEditor/Images/DiagonalRed.png


BIN
PixiEditor/Images/Folder-add.png


BIN
PixiEditor/Images/Folder.png


BIN
PixiEditor/Images/Layer-add.png


BIN
PixiEditor/Images/Trash.png


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

@@ -125,18 +125,18 @@ namespace PixiEditor.Models.Controllers
 
                 if (IsOperationTool(SelectedTool))
                 {
-                    BitmapOperations.ExecuteTool(newPosition, MouseController.LastMouseMoveCoordinates.ToList(), (BitmapOperationTool)SelectedTool);
+                    BitmapOperations.ExecuteTool(newPosition, MouseController.LastMouseMoveCoordinates, (BitmapOperationTool)SelectedTool);
                 }
                 else
                 {
-                    ReadonlyToolUtility.ExecuteTool(MouseController.LastMouseMoveCoordinates.ToArray(), (ReadonlyTool)SelectedTool);
+                    ReadonlyToolUtility.ExecuteTool(MouseController.LastMouseMoveCoordinates, (ReadonlyTool)SelectedTool);
                 }
             }
         }
 
         public WriteableBitmap GetCombinedLayersBitmap()
         {
-            return BitmapUtils.CombineLayers(ActiveDocument.Width, ActiveDocument.Height, ActiveDocument.Layers.Where(x => x.IsVisible).ToArray());
+            return BitmapUtils.CombineLayers(ActiveDocument.Width, ActiveDocument.Height, ActiveDocument.Layers.Where(x => ActiveDocument.GetFinalLayerIsVisible(x)).ToArray(), ActiveDocument.LayerStructure);
         }
 
         /// <summary>

+ 48 - 8
PixiEditor/Models/Controllers/BitmapOperationsUtility.cs

@@ -11,6 +11,7 @@ using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
+using PixiEditor.Models.Tools.ToolSettings.Settings;
 using PixiEditor.Models.Undo;
 using PixiEditor.ViewModels;
 
@@ -24,6 +25,8 @@ namespace PixiEditor.Models.Controllers
 
         private Coordinates lastMousePos;
 
+        private SizeSetting sizeSetting;
+
         public BitmapOperationsUtility(BitmapManager manager)
         {
             Manager = manager;
@@ -57,7 +60,7 @@ namespace PixiEditor.Models.Controllers
         }
 
         /// <summary>
-        ///     Executes tool Use() method with given parameters. NOTE: mouseMove is reversed inside function!.
+        ///     Executes tool Use() method with given parameters. NOTE: [0] is a start point, [^1] is latest.
         /// </summary>
         /// <param name="newPos">Most recent coordinates.</param>
         /// <param name="mouseMove">Last mouse movement coordinates.</param>
@@ -71,7 +74,6 @@ namespace PixiEditor.Models.Controllers
                     return;
                 }
 
-                mouseMove.Reverse();
                 UseTool(mouseMove, tool, Manager.PrimaryColor);
 
                 lastMousePos = newPos;
@@ -118,14 +120,32 @@ namespace PixiEditor.Models.Controllers
 
         private void UseTool(List<Coordinates> mouseMoveCords, BitmapOperationTool tool, Color color)
         {
-            if (Keyboard.IsKeyDown(Key.LeftShift) && !MouseCordsNotInLine(mouseMoveCords))
+            if(sizeSetting == null)
+            {
+                sizeSetting = tool.Toolbar.GetSetting<SizeSetting>("ToolSize");
+            }
+
+            int thickness = sizeSetting != null ? sizeSetting.Value : 1;
+
+            bool shiftDown = Keyboard.IsKeyDown(Key.LeftShift);
+
+            if (shiftDown)
             {
-                mouseMoveCords = GetSquareCoordiantes(mouseMoveCords);
+                bool mouseInLine = MouseCordsNotInLine(mouseMoveCords, thickness);
+
+                if (!mouseInLine)
+                {
+                    mouseMoveCords = GetSquareCoordiantes(mouseMoveCords);
+                }
+                else
+                {
+                    mouseMoveCords = GetLineCoordinates(mouseMoveCords, thickness);
+                }
             }
 
             if (!tool.RequiresPreviewLayer)
             {
-                LayerChange[] modifiedLayers = tool.Use(Manager.ActiveLayer, mouseMoveCords.ToArray(), color);
+                LayerChange[] modifiedLayers = tool.Use(Manager.ActiveLayer, mouseMoveCords, color);
                 LayerChange[] oldPixelsValues = new LayerChange[modifiedLayers.Length];
                 for (int i = 0; i < modifiedLayers.Length; i++)
                 {
@@ -173,9 +193,29 @@ namespace PixiEditor.Models.Controllers
             return oldPixelValues;
         }
 
-        private bool MouseCordsNotInLine(List<Coordinates> cords)
+        private bool MouseCordsNotInLine(List<Coordinates> cords, int thickness)
         {
-            return cords[0].X == cords[^1].X || cords[0].Y == cords[^1].Y;
+            return (cords[0].X > cords[^1].X - thickness && cords[0].X < cords[^1].X + thickness)
+                || (cords[0].Y > cords[^1].Y - thickness && cords[0].Y < cords[^1].Y + thickness);
+        }
+
+        private List<Coordinates> GetLineCoordinates(List<Coordinates> mouseMoveCords, int thickness)
+        {
+            int y = mouseMoveCords[0].Y;
+            int x = mouseMoveCords[0].X;
+
+
+            if (Math.Abs(mouseMoveCords[^1].X - mouseMoveCords[0].X) - thickness > 0)
+            {
+                y = mouseMoveCords[^1].Y;
+            }
+            else
+            {
+                x = mouseMoveCords[^1].X;
+            }
+
+            mouseMoveCords[0] = new Coordinates(x, y);
+            return mouseMoveCords;
         }
 
         /// <summary>
@@ -228,7 +268,7 @@ namespace PixiEditor.Models.Controllers
 
                 modifiedLayers = ((BitmapOperationTool)Manager.SelectedTool).Use(
                     Manager.ActiveDocument.ActiveLayer,
-                    mouseMove.ToArray(),
+                    mouseMove,
                     Manager.PrimaryColor);
 
                 BitmapPixelChanges[] changes = modifiedLayers.Select(x => x.PixelChanges).ToArray();

+ 132 - 131
PixiEditor/Models/Controllers/ClipboardController.cs

@@ -1,131 +1,132 @@
-using PixiEditor.Models.ImageManipulation;
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Position;
-using PixiEditor.Models.Undo;
-using PixiEditor.ViewModels;
-using System.IO;
-using System.Linq;
-using System.Windows;
-using System.Windows.Media.Imaging;
-
-namespace PixiEditor.Models.Controllers
-{
-    public static class ClipboardController
-    {
-        /// <summary>
-        ///     Copies selection to clipboard in PNG, Bitmap and DIB formats.
-        /// </summary>
-        /// <param name="layers">Layers where selection is.</param>
-        public static void CopyToClipboard(Layer[] layers, Coordinates[] selection, int originalImageWidth, int originalImageHeight)
-        {
-            Clipboard.Clear();
-            WriteableBitmap combinedBitmaps = BitmapUtils.CombineLayers(originalImageWidth, originalImageHeight, layers);
-            using (MemoryStream pngStream = new MemoryStream())
-            {
-                DataObject data = new DataObject();
-                BitmapSource croppedBmp = BitmapSelectionToBmpSource(combinedBitmaps, selection);
-                data.SetData(DataFormats.Bitmap, croppedBmp, true); // Bitmap, no transparency support
-
-                PngBitmapEncoder encoder = new PngBitmapEncoder();
-                encoder.Frames.Add(BitmapFrame.Create(croppedBmp));
-                encoder.Save(pngStream);
-                data.SetData("PNG", pngStream, false); // PNG, supports transparency
-
-                Clipboard.SetImage(croppedBmp); // DIB format
-                Clipboard.SetDataObject(data, true);
-            }
-        }
-
-        /// <summary>
-        ///     Pastes image from clipboard into new layer.
-        /// </summary>
-        public static void PasteFromClipboard()
-        {
-            WriteableBitmap image = GetImageFromClipboard();
-            if (image != null)
-            {
-                AddImageToLayers(image);
-                int latestLayerIndex = ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.Count - 1;
-                ViewModelMain.Current.BitmapManager.ActiveDocument.UndoManager.AddUndoChange(
-                    new Change(RemoveLayerProcess, new object[] { latestLayerIndex }, AddLayerProcess, new object[] { 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)
-                {
-                    if (pngStream != null)
-                    {
-                        PngBitmapDecoder decoder = new PngBitmapDecoder(pngStream, BitmapCreateOptions.IgnoreImageCache, BitmapCacheOption.OnLoad);
-                        finalImage = new WriteableBitmap(decoder.Frames[0].Clone());
-                    }
-                }
-            }
-            else if (dao.GetDataPresent(DataFormats.Dib))
-            {
-                finalImage = new WriteableBitmap(Clipboard.GetImage()!);
-            }
-            else if (dao.GetDataPresent(DataFormats.Bitmap))
-            {
-                finalImage = new WriteableBitmap((dao.GetData(DataFormats.Bitmap) as BitmapSource)!);
-            }
-
-            return finalImage;
-        }
-
-        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);
-        }
-
-        public static BitmapSource BitmapSelectionToBmpSource(WriteableBitmap bitmap, Coordinates[] selection)
-        {
-            int offsetX = selection.Min(x => x.X);
-            int offsetY = selection.Min(x => x.Y);
-            int width = selection.Max(x => x.X) - offsetX + 1;
-            int height = selection.Max(x => x.Y) - offsetY + 1;
-            return bitmap.Crop(offsetX, offsetY, width, height);
-        }
-
-        private static void AddImageToLayers(WriteableBitmap image)
-        {
-            ViewModelMain.Current.BitmapManager.ActiveDocument.AddNewLayer("Image", image);
-        }
-
-        private static void RemoveLayerProcess(object[] parameters)
-        {
-            if (parameters.Length == 0 || !(parameters[0] is int))
-            {
-                return;
-            }
-
-            ViewModelMain.Current.BitmapManager.ActiveDocument.RemoveLayer((int)parameters[0]);
-        }
-
-        private static void AddLayerProcess(object[] parameters)
-        {
-            if (parameters.Length == 0 || !(parameters[0] is WriteableBitmap))
-            {
-                return;
-            }
-
-            AddImageToLayers((WriteableBitmap)parameters[0]);
-        }
-    }
-}
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
+using PixiEditor.ViewModels;
+using System.IO;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.Models.Controllers
+{
+    public static class ClipboardController
+    {
+        /// <summary>
+        ///     Copies selection to clipboard in PNG, Bitmap and DIB formats.
+        /// </summary>
+        /// <param name="layers">Layers where selection is.</param>
+        public static void CopyToClipboard(Layer[] layers, Coordinates[] selection, int originalImageWidth, int originalImageHeight)
+        {
+            Clipboard.Clear();
+            WriteableBitmap combinedBitmaps = BitmapUtils.CombineLayers(originalImageWidth, originalImageHeight, layers);
+            using (MemoryStream pngStream = new MemoryStream())
+            {
+                DataObject data = new DataObject();
+                BitmapSource croppedBmp = BitmapSelectionToBmpSource(combinedBitmaps, selection);
+                data.SetData(DataFormats.Bitmap, croppedBmp, true); // Bitmap, no transparency support
+
+                PngBitmapEncoder encoder = new PngBitmapEncoder();
+                encoder.Frames.Add(BitmapFrame.Create(croppedBmp));
+                encoder.Save(pngStream);
+                data.SetData("PNG", pngStream, false); // PNG, supports transparency
+
+                Clipboard.SetImage(croppedBmp); // DIB format
+                Clipboard.SetDataObject(data, true);
+            }
+        }
+
+        /// <summary>
+        ///     Pastes image from clipboard into new layer.
+        /// </summary>
+        public static void PasteFromClipboard()
+        {
+            WriteableBitmap image = GetImageFromClipboard();
+            if (image != null)
+            {
+                AddImageToLayers(image);
+                int latestLayerIndex = ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.Count - 1;
+                ViewModelMain.Current.BitmapManager.ActiveDocument.UndoManager.AddUndoChange(
+                    new Change(RemoveLayerProcess, new object[] { latestLayerIndex }, AddLayerProcess, new object[] { 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)
+                {
+                    if (pngStream != null)
+                    {
+                        PngBitmapDecoder decoder = new PngBitmapDecoder(pngStream, BitmapCreateOptions.IgnoreImageCache, BitmapCacheOption.OnLoad);
+                        finalImage = new WriteableBitmap(decoder.Frames[0].Clone());
+                    }
+                }
+            }
+            else if (dao.GetDataPresent(DataFormats.Dib))
+            {
+                finalImage = new WriteableBitmap(Clipboard.GetImage()!);
+            }
+            else if (dao.GetDataPresent(DataFormats.Bitmap))
+            {
+                finalImage = new WriteableBitmap((dao.GetData(DataFormats.Bitmap) as BitmapSource)!);
+            }
+
+            return finalImage;
+        }
+
+        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);
+        }
+
+        public static BitmapSource BitmapSelectionToBmpSource(WriteableBitmap bitmap, Coordinates[] selection)
+        {
+            int offsetX = selection.Min(x => x.X);
+            int offsetY = selection.Min(x => x.Y);
+            int width = selection.Max(x => x.X) - offsetX + 1;
+            int height = selection.Max(x => x.Y) - offsetY + 1;
+            return bitmap.Crop(offsetX, offsetY, width, height);
+        }
+
+        private static void RemoveLayerProcess(object[] parameters)
+        {
+            if (parameters.Length == 0 || !(parameters[0] is int))
+            {
+                return;
+            }
+
+            ViewModelMain.Current.BitmapManager.ActiveDocument.RemoveLayer((int)parameters[0], true);
+        }
+
+        private static void AddLayerProcess(object[] parameters)
+        {
+            if (parameters.Length == 0 || !(parameters[0] is WriteableBitmap))
+            {
+                return;
+            }
+
+            AddImageToLayers((WriteableBitmap)parameters[0]);
+        }
+
+        private static void AddImageToLayers(WriteableBitmap image)
+        {
+            ViewModelMain.Current.BitmapManager.ActiveDocument.AddNewLayer("Image", image);
+        }
+    }
+}

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

@@ -40,7 +40,7 @@ namespace PixiEditor.Models.Controllers
             {
                 if (LastMouseMoveCoordinates.Count == 0 || mouseCoordinates != LastMouseMoveCoordinates[^1])
                 {
-                    LastMouseMoveCoordinates.Add(mouseCoordinates);
+                    LastMouseMoveCoordinates.Insert(0, mouseCoordinates);
                     MousePositionChanged?.Invoke(this, new MouseMovementEventArgs(mouseCoordinates));
                 }
             }

+ 3 - 2
PixiEditor/Models/Controllers/ReadonlyToolUtility.cs

@@ -1,11 +1,12 @@
-using PixiEditor.Models.Position;
+using System.Collections.Generic;
+using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 
 namespace PixiEditor.Models.Controllers
 {
     public class ReadonlyToolUtility
     {
-        public void ExecuteTool(Coordinates[] mouseMove, ReadonlyTool tool)
+        public void ExecuteTool(List<Coordinates> mouseMove, ReadonlyTool tool)
         {
             tool.Use(mouseMove);
         }

+ 59 - 0
PixiEditor/Models/Controllers/UndoManager.cs

@@ -99,6 +99,65 @@ namespace PixiEditor.Models.Controllers
             UndoStack.Push(change);
         }
 
+        /// <summary>
+        /// Merges multiple undo changes into one.
+        /// </summary>
+        /// <param name="amount">Amount of changes to squash.</param>
+        public void SquashUndoChanges(int amount)
+        {
+            string description = UndoStack.ElementAt(UndoStack.Count - amount).Description;
+            SquashUndoChanges(amount, description);
+        }
+
+        /// <summary>
+        /// Merges multiple undo changes into one.
+        /// </summary>
+        /// <param name="amount">Amount of changes to squash.</param>
+        /// <param name="description">Final change description.</param>
+        public void SquashUndoChanges(int amount, string description)
+        {
+            Change[] changes = new Change[amount];
+            for (int i = 0; i < amount; i++)
+            {
+                changes[i] = UndoStack.Pop();
+            }
+
+            Action<object[]> reverseProcess = (object[] props) =>
+            {
+                foreach (var prop in props)
+                {
+                    Change change = (Change)prop;
+                    if (change.ReverseProcess == null)
+                    {
+                        SetPropertyValue(GetChangeRoot(change), change.Property, change.OldValue);
+                    }
+                    else
+                    {
+                        change.ReverseProcess(change.ReverseProcessArguments);
+                    }
+                }
+            };
+
+            Action<object[]> process = (object[] props) =>
+            {
+                foreach (var prop in props.Reverse())
+                {
+                    Change change = (Change)prop;
+                    if (change.Process == null)
+                    {
+                        SetPropertyValue(GetChangeRoot(change), change.Property, change.NewValue);
+                    }
+                    else
+                    {
+                        change.Process(change.ProcessArguments);
+                    }
+                }
+            };
+
+            Change change = new(reverseProcess, changes, process, changes, description);
+            AddUndoChange(change);
+        }
+
         private bool ChangeIsBlockedProperty(Change change)
         {
             return (change.Root != null || change.FindRootProcess != null)

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

@@ -1,5 +1,7 @@
 using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Layers;
 using PixiEditor.ViewModels;
+using System.Linq;
 
 namespace PixiEditor.Models.DataHolders
 {
@@ -17,8 +19,36 @@ namespace PixiEditor.Models.DataHolders
         {
             SetRelayCommands();
             UndoManager = new UndoManager();
+            LayerStructure = new LayerStructure(this);
             XamlAccesibleViewModel = ViewModelMain.Current;
             GeneratePreviewLayer();
+            Layers.CollectionChanged += Layers_CollectionChanged;
+            LayerStructure.Groups.CollectionChanged += Groups_CollectionChanged;
+            LayerStructure.LayerStructureChanged += LayerStructure_LayerStructureChanged;
+        }
+
+        private void LayerStructure_LayerStructureChanged(object sender, LayerStructureChangedEventArgs e)
+        {
+            RaisePropertyChanged(nameof(LayerStructure));
+            foreach (var layerGuid in e.AffectedLayerGuids)
+            {
+                Layer layer = Layers.First(x => x.LayerGuid == layerGuid);
+                layer.RaisePropertyChange(nameof(layer.IsVisible));
+                layer.RaisePropertyChange(nameof(layer.Opacity));
+            }
+        }
+
+        private void Groups_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+        {
+            if (e.OldItems != e.NewItems)
+            {
+                RaisePropertyChanged(nameof(LayerStructure));
+            }
+        }
+
+        private void Layers_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+        {
+            RaisePropertyChanged(nameof(Layers));
         }
     }
 }

+ 308 - 44
PixiEditor/Models/DataHolders/Document/Document.Layers.cs

@@ -1,4 +1,12 @@
-using System;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
+using System;
+using System.Buffers;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
@@ -6,13 +14,6 @@ using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
-using PixiEditor.Helpers;
-using PixiEditor.Models.Controllers;
-using PixiEditor.Models.Enums;
-using PixiEditor.Models.ImageManipulation;
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Position;
-using PixiEditor.Models.Undo;
 
 namespace PixiEditor.Models.DataHolders
 {
@@ -20,10 +21,31 @@ namespace PixiEditor.Models.DataHolders
     {
         public const string MainSelectedLayerColor = "#505056";
         public const string SecondarySelectedLayerColor = "#7D505056";
-        private static readonly Regex reversedLayerSuffixRegex = new (@"(?:\)([0-9]+)*\()? *([\s\S]+)", RegexOptions.Compiled);
+        private static readonly Regex reversedLayerSuffixRegex = new(@"(?:\)([0-9]+)*\()? *([\s\S]+)", RegexOptions.Compiled);
         private Guid activeLayerGuid;
+        private LayerStructure layerStructure;
 
-        public ObservableCollection<Layer> Layers { get; set; } = new ObservableCollection<Layer>();
+        private ObservableCollection<Layer> layers = new();
+
+        public ObservableCollection<Layer> Layers
+        {
+            get => layers;
+            set
+            {
+                layers = value;
+                Layers.CollectionChanged += Layers_CollectionChanged;
+            }
+        }
+
+        public LayerStructure LayerStructure
+        {
+            get => layerStructure;
+            set
+            {
+                layerStructure = value;
+                RaisePropertyChanged(nameof(LayerStructure));
+            }
+        }
 
         public Layer ActiveLayer => Layers.Count > 0 ? Layers.FirstOrDefault(x => x.LayerGuid == ActiveLayerGuid) : null;
 
@@ -32,9 +54,12 @@ namespace PixiEditor.Models.DataHolders
             get => activeLayerGuid;
             set
             {
-                activeLayerGuid = value;
-                RaisePropertyChanged(nameof(ActiveLayerGuid));
-                RaisePropertyChanged(nameof(ActiveLayer));
+                if (value != activeLayerGuid)
+                {
+                    activeLayerGuid = value;
+                    RaisePropertyChanged(nameof(ActiveLayerGuid));
+                    RaisePropertyChanged(nameof(ActiveLayer));
+                }
             }
         }
 
@@ -60,6 +85,35 @@ namespace PixiEditor.Models.DataHolders
             LayersChanged?.Invoke(this, new LayersChangedEventArgs(ActiveLayerGuid, LayerAction.SetActive));
         }
 
+        /// <summary>
+        /// Gets final layer IsVisible taking into consideration group visibility.
+        /// </summary>
+        /// <param name="layer">Layer to check.</param>
+        /// <returns>True if is visible, false if at least parent is not visible or layer itself is invisible.</returns>
+        public bool GetFinalLayerIsVisible(Layer layer)
+        {
+            if (!layer.IsVisible)
+            {
+                return false;
+            }
+
+            var group = LayerStructure.GetGroupByLayer(layer.LayerGuid);
+            bool atLeastOneParentIsInvisible = false;
+            GuidStructureItem groupToCheck = group;
+            while (groupToCheck != null)
+            {
+                if (!groupToCheck.IsVisible)
+                {
+                    atLeastOneParentIsInvisible = true;
+                    break;
+                }
+
+                groupToCheck = groupToCheck.Parent;
+            }
+
+            return !atLeastOneParentIsInvisible;
+        }
+
         public void UpdateLayersColor()
         {
             foreach (var layer in Layers)
@@ -75,16 +129,65 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
-        public void MoveLayerIndexBy(int layerIndex, int amount)
+        public void MoveLayerInStructure(Guid layerGuid, Guid referenceLayer, bool above = false)
         {
-            MoveLayerProcess(new object[] { layerIndex, amount });
+            var args = new object[] { layerGuid, referenceLayer, above };
+
+            Layer layer = Layers.First(x => x.LayerGuid == layerGuid);
+
+            int oldIndex = Layers.IndexOf(layer);
+
+            var oldLayerStrcutureGroups = LayerStructure.CloneGroups();
+
+            MoveLayerInStructureProcess(args);
+
+            AddLayerStructureToUndo(oldLayerStrcutureGroups);
 
             UndoManager.AddUndoChange(new Change(
-                MoveLayerProcess,
-                new object[] { layerIndex + amount, -amount },
-                MoveLayerProcess,
-                new object[] { layerIndex, amount },
+                ReverseMoveLayerInStructureProcess,
+                new object[] { oldIndex, layerGuid },
+                MoveLayerInStructureProcess,
+                args,
                 "Move layer"));
+
+            UndoManager.SquashUndoChanges(2, "Move layer");
+        }
+
+        public void MoveGroupInStructure(Guid groupGuid, Guid referenceLayer, bool above = false)
+        {
+            var args = new object[] { groupGuid, referenceLayer, above };
+
+            var topLayer = Layers.First(x => x.LayerGuid == LayerStructure.GetGroupByGuid(groupGuid).EndLayerGuid);
+            var bottomLayer = Layers.First(x => x.LayerGuid == LayerStructure.GetGroupByGuid(groupGuid).StartLayerGuid);
+
+            int indexOfTopLayer = Layers.IndexOf(topLayer);
+            Guid oldReferenceLayerGuid;
+            bool oldAbove = false;
+
+            if (indexOfTopLayer + 1 < Layers.Count)
+            {
+                oldReferenceLayerGuid = topLayer.LayerGuid;
+            }
+            else
+            {
+                int indexOfBottomLayer = Layers.IndexOf(bottomLayer);
+                oldReferenceLayerGuid = Layers[indexOfBottomLayer - 1].LayerGuid;
+                oldAbove = true;
+            }
+
+            var oldLayerStructure = LayerStructure.CloneGroups();
+
+            MoveGroupInStructureProcess(args);
+
+            AddLayerStructureToUndo(oldLayerStructure);
+
+            UndoManager.AddUndoChange(new Change(
+                MoveGroupInStructureProcess,
+                new object[] { groupGuid, oldReferenceLayerGuid, oldAbove },
+                MoveGroupInStructureProcess,
+                args));
+
+            UndoManager.SquashUndoChanges(2, "Move group");
         }
 
         public void AddNewLayer(string name, WriteableBitmap bitmap, bool setAsActive = true)
@@ -117,7 +220,7 @@ namespace PixiEditor.Models.DataHolders
 
             if (Layers.Count > 1)
             {
-                StorageBasedChange storageChange = new StorageBasedChange(this, new[] { Layers[^1] }, false);
+                StorageBasedChange storageChange = new(this, new[] { Layers[^1] }, false);
                 UndoManager.AddUndoChange(
                     storageChange.ToChange(
                         RemoveLayerProcess,
@@ -126,7 +229,7 @@ namespace PixiEditor.Models.DataHolders
                         "Add layer"));
             }
 
-            LayersChanged?.Invoke(this, new LayersChangedEventArgs(Layers[0].LayerGuid, LayerAction.Add));
+            LayersChanged?.Invoke(this, new LayersChangedEventArgs(Layers[^1].LayerGuid, LayerAction.Add));
         }
 
         /// <summary>
@@ -143,7 +246,7 @@ namespace PixiEditor.Models.DataHolders
             Layers.Insert(index + 1, duplicate);
             SetMainActiveLayer(index + 1);
 
-            StorageBasedChange storageChange = new (this, new[] { duplicate }, false);
+            StorageBasedChange storageChange = new(this, new[] { duplicate }, false);
             UndoManager.AddUndoChange(
                 storageChange.ToChange(
                     RemoveLayerProcess,
@@ -231,18 +334,23 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
-        public void RemoveLayer(int layerIndex)
+        public void RemoveLayer(int layerIndex, bool addToUndo = true)
         {
             if (Layers.Count == 0)
             {
                 return;
             }
 
+            LayerStructure.AssignParent(Layers[layerIndex].LayerGuid, null);
+
             bool wasActive = Layers[layerIndex].IsActive;
 
-            StorageBasedChange change = new StorageBasedChange(this, new[] { Layers[layerIndex] });
-            UndoManager.AddUndoChange(
-                change.ToChange(RestoreLayersProcess, RemoveLayerProcess, new object[] { Layers[layerIndex].LayerGuid }, "Remove layer"));
+            StorageBasedChange change = new(this, new[] { Layers[layerIndex] });
+            if (addToUndo)
+            {
+                UndoManager.AddUndoChange(
+                    change.ToChange(RestoreLayersProcess, RemoveLayerProcess, new object[] { Layers[layerIndex].LayerGuid }));
+            }
 
             Layers.RemoveAt(layerIndex);
             if (wasActive)
@@ -251,9 +359,9 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
-        public void RemoveLayer(Layer layer)
+        public void RemoveLayer(Layer layer, bool addToUndo)
         {
-            RemoveLayer(Layers.IndexOf(layer));
+            RemoveLayer(Layers.IndexOf(layer), addToUndo);
         }
 
         public void RemoveActiveLayers()
@@ -263,18 +371,35 @@ namespace PixiEditor.Models.DataHolders
                 return;
             }
 
+            var oldLayerStructure = LayerStructure.CloneGroups();
+
             Layer[] layers = Layers.Where(x => x.IsActive).ToArray();
             int firstIndex = Layers.IndexOf(layers[0]);
 
             object[] guidArgs = new object[] { layers.Select(x => x.LayerGuid).ToArray() };
 
-            StorageBasedChange change = new StorageBasedChange(this, layers);
+            StorageBasedChange change = new(this, layers);
 
             RemoveLayersProcess(guidArgs);
 
+            AddLayerStructureToUndo(oldLayerStructure);
+
             InjectRemoveActiveLayersUndo(guidArgs, change);
 
+            UndoManager.SquashUndoChanges(2, "Removed active layers");
+
             SetNextLayerAsActive(firstIndex);
+
+        }
+
+        public void AddLayerStructureToUndo(ObservableCollection<GuidStructureItem> oldLayerStructureGroups)
+        {
+            UndoManager.AddUndoChange(
+                new Change(
+                    BuildLayerStructureProcess,
+                    new[] { oldLayerStructureGroups },
+                    BuildLayerStructureProcess,
+                    new[] { LayerStructure.CloneGroups() }));
         }
 
         public Layer MergeLayers(Layer[] layersToMerge, bool nameOfLast, int index)
@@ -286,7 +411,7 @@ namespace PixiEditor.Models.DataHolders
 
             string name;
 
-            // Wich name should be used
+            // Which name should be used
             if (nameOfLast)
             {
                 name = layersToMerge[^1].Name;
@@ -298,17 +423,26 @@ namespace PixiEditor.Models.DataHolders
 
             Layer mergedLayer = layersToMerge[0];
 
+            var groupParent = LayerStructure.GetGroupByLayer(layersToMerge[^1].LayerGuid);
+
+            Layer placeholderLayer = new("_placeholder");
+            Layers.Insert(index, placeholderLayer);
+            LayerStructure.AssignParent(placeholderLayer.LayerGuid, groupParent?.GroupGuid);
+
             for (int i = 0; i < layersToMerge.Length - 1; i++)
             {
                 Layer firstLayer = mergedLayer;
                 Layer secondLayer = layersToMerge[i + 1];
                 mergedLayer = firstLayer.MergeWith(secondLayer, name, Width, Height);
-                Layers.Remove(layersToMerge[i]);
+                RemoveLayer(layersToMerge[i], false);
             }
 
-            Layers.Remove(layersToMerge[^1]);
-
             Layers.Insert(index, mergedLayer);
+            LayerStructure.AssignParent(mergedLayer.LayerGuid, groupParent?.GroupGuid);
+
+            RemoveLayer(placeholderLayer, false);
+
+            RemoveLayer(layersToMerge[^1], false);
 
             SetMainActiveLayer(Layers.IndexOf(mergedLayer));
 
@@ -324,18 +458,23 @@ namespace PixiEditor.Models.DataHolders
 
             IEnumerable<Layer> undoArgs = layersToMerge;
 
-            StorageBasedChange undoChange = new StorageBasedChange(this, undoArgs);
+            var oldLayerStructure = LayerStructure.CloneGroups();
+
+            StorageBasedChange undoChange = new(this, undoArgs);
 
             int[] indexes = layersToMerge.Select(x => Layers.IndexOf(x)).ToArray();
 
             var layer = MergeLayers(layersToMerge, nameIsLastLayers, Layers.IndexOf(layersToMerge[0]));
 
+            AddLayerStructureToUndo(oldLayerStructure);
+
             UndoManager.AddUndoChange(undoChange.ToChange(
                 InsertLayersAtIndexesProcess,
                 new object[] { indexes[0] },
                 MergeLayersProcess,
-                new object[] { indexes, nameIsLastLayers, layer.LayerGuid },
-                "Undo merge layers"));
+                new object[] { indexes, nameIsLastLayers, layer.LayerGuid }));
+
+            UndoManager.SquashUndoChanges(2, "Undo merge layers");
 
             return layer;
         }
@@ -345,6 +484,37 @@ namespace PixiEditor.Models.DataHolders
             return BitmapUtils.GetColorAtPointCombined(x, y, Layers.ToArray());
         }
 
+        private void BuildLayerStructureProcess(object[] parameters)
+        {
+            if (parameters.Length > 0 && parameters[0] is ObservableCollection<GuidStructureItem> groups)
+            {
+                LayerStructure.Groups.CollectionChanged -= Groups_CollectionChanged;
+                LayerStructure.Groups = LayerStructure.CloneGroups(groups);
+                LayerStructure.Groups.CollectionChanged += Groups_CollectionChanged;
+                RaisePropertyChanged(nameof(LayerStructure));
+            }
+        }
+
+        private void ReverseMoveLayerInStructureProcess(object[] props)
+        {
+            int indexTo = (int)props[0];
+            Guid layerGuid = (Guid)props[1];
+
+            Guid layerAtOldIndex = Layers[indexTo].LayerGuid;
+
+            var startGroup = LayerStructure.GetGroupByLayer(layerGuid);
+
+            LayerStructure.PreMoveReassignBounds(new GroupData(startGroup?.GroupGuid), layerGuid);
+
+            Layers.Move(Layers.IndexOf(Layers.First(x => x.LayerGuid == layerGuid)), indexTo);
+
+            var newGroup = LayerStructure.GetGroupByLayer(layerAtOldIndex);
+
+            LayerStructure.PostMoveReassignBounds(new GroupData(newGroup?.GroupGuid), layerGuid);
+
+            RaisePropertyChanged(nameof(LayerStructure));
+        }
+
         private void InjectRemoveActiveLayersUndo(object[] guidArgs, StorageBasedChange change)
         {
             Action<Layer[], UndoLayer[]> undoAction = RestoreLayersProcess;
@@ -352,7 +522,7 @@ namespace PixiEditor.Models.DataHolders
 
             if (Layers.Count == 0)
             {
-                Layer layer = new Layer("Base Layer");
+                Layer layer = new("Base Layer", 0, 0) { MaxHeight = Height, MaxWidth = Width };
                 Layers.Add(layer);
                 undoAction = (Layer[] layers, UndoLayer[] undoData) =>
                 {
@@ -434,16 +604,80 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
-        private void MoveLayerProcess(object[] parameter)
+        private void MoveGroupInStructureProcess(object[] parameter)
         {
-            int layerIndex = (int)parameter[0];
-            int amount = (int)parameter[1];
+            Guid groupGuid = (Guid)parameter[0];
+            Guid referenceLayerGuid = (Guid)parameter[1];
+            bool above = (bool)parameter[2];
+
+            GuidStructureItem group = LayerStructure.GetGroupByGuid(groupGuid);
+            GuidStructureItem referenceLayerGroup = LayerStructure.GetGroupByLayer(referenceLayerGuid);
+
+            Layer referenceLayer = Layers.First(x => x.LayerGuid == referenceLayerGuid);
 
-            Layers.Move(layerIndex, layerIndex + amount);
-            if (Layers.IndexOf(ActiveLayer) == layerIndex)
+            int layerIndex = Layers.IndexOf(referenceLayer);
+            int folderTopIndex = Layers.IndexOf(Layers.First(x => x.LayerGuid == group?.EndLayerGuid));
+            int oldIndex = folderTopIndex;
+
+            if (layerIndex < folderTopIndex)
             {
-                SetMainActiveLayer(layerIndex + amount);
+                int folderBottomIndex = Layers.IndexOf(Layers.First(x => x.LayerGuid == group.StartLayerGuid));
+                oldIndex = folderBottomIndex;
             }
+
+            int newIndex = CalculateNewIndex(layerIndex, above, oldIndex);
+
+            LayerStructure.MoveGroup(groupGuid, newIndex);
+
+            LayerStructure.ReassignParent(group, referenceLayerGroup);
+
+            LayerStructure.PostMoveReassignBounds(new GroupData(group?.Parent?.GroupGuid), new GroupData(group?.GroupGuid));
+        }
+
+        private int CalculateNewIndex(int layerIndex, bool above, int oldIndex)
+        {
+            int newIndex = layerIndex;
+
+            int diff = newIndex - oldIndex;
+
+            if (TriesToMoveAboveBelow(above, diff) || TriesToMoveBelowAbove(above, diff) || (above && newIndex < oldIndex) || (!above && newIndex > oldIndex))
+            {
+                newIndex += above ? 1 : -1;
+            }
+
+            return Math.Clamp(newIndex, 0, Layers.Count - 1);
+        }
+
+        private bool TriesToMoveAboveBelow(bool above, int diff) => above && diff == -1;
+
+        private bool TriesToMoveBelowAbove(bool above, int diff) => !above && diff == 1;
+
+        private void MoveLayerInStructureProcess(object[] parameter)
+        {
+            Guid layer = (Guid)parameter[0];
+            Guid referenceLayer = (Guid)parameter[1];
+            bool above = (bool)parameter[2];
+
+            int layerIndex = Layers.IndexOf(Layers.First(x => x.LayerGuid == referenceLayer));
+            int oldIndex = Layers.IndexOf(Layers.First(x => x.LayerGuid == layer));
+            int newIndex = CalculateNewIndex(layerIndex, above, oldIndex);
+
+            var startGroup = LayerStructure.GetGroupByLayer(layer);
+
+            LayerStructure.PreMoveReassignBounds(new GroupData(startGroup?.GroupGuid), layer);
+
+            Layers.Move(oldIndex, newIndex);
+
+            var newFolder = LayerStructure.GetGroupByLayer(referenceLayer);
+
+            LayerStructure.PostMoveReassignBounds(new GroupData(newFolder?.GroupGuid), layer);
+
+            if (Layers.IndexOf(ActiveLayer) == oldIndex)
+            {
+                SetMainActiveLayer(newIndex);
+            }
+
+            RaisePropertyChanged(nameof(LayerStructure));
         }
 
         private void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
@@ -467,12 +701,42 @@ namespace PixiEditor.Models.DataHolders
                 Layer layer = Layers.First(x => x.LayerGuid == layerGuid);
                 int index = Layers.IndexOf(layer);
                 bool wasActive = layer.IsActive;
+
+                var layerGroup = LayerStructure.GetGroupByLayer(layer.LayerGuid);
+
+                LayerStructure.ExpandParentGroups(layerGroup);
+
+                if (layerGroup?.Parent != null && LayerStructure.GroupContainsOnlyLayer(layer.LayerGuid, layerGroup))
+                {
+                    LayerStructure.PreMoveReassignBounds(new GroupData(layerGroup.Parent.GroupGuid), new GroupData(layerGroup.GroupGuid));
+                }
+                LayerStructure.AssignParent(Layers[index].LayerGuid, null);
+                RemoveGroupsIfEmpty(layer, layerGroup);
+
                 Layers.Remove(layer);
 
                 if (wasActive || Layers.IndexOf(ActiveLayer) >= index)
                 {
                     SetNextLayerAsActive(index);
                 }
+
+                LayersChanged?.Invoke(this, new LayersChangedEventArgs(layerGuid, LayerAction.Remove));
+            }
+        }
+
+        private void RemoveGroupsIfEmpty(Layer layer, GuidStructureItem layerGroup)
+        {
+            if (LayerStructure.GroupContainsOnlyLayer(layer.LayerGuid, layerGroup))
+            {
+                if (layerGroup.Parent != null)
+                {
+                    layerGroup.Parent.Subgroups.Remove(layerGroup);
+                    RemoveGroupsIfEmpty(layer, layerGroup.Parent);
+                }
+                else
+                {
+                    LayerStructure.Groups.Remove(layerGroup);
+                }
             }
         }
 
@@ -554,4 +818,4 @@ namespace PixiEditor.Models.DataHolders
             }
         }
     }
-}
+}

+ 6 - 10
PixiEditor/Models/DataHolders/Document/Document.Operations.cs

@@ -1,14 +1,10 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
-using PixiEditor.Helpers.Extensions;
+using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Undo;
+using System;
+using System.Linq;
+using System.Windows;
 
 namespace PixiEditor.Models.DataHolders
 {
@@ -101,7 +97,7 @@ namespace PixiEditor.Models.DataHolders
 
         private void ResizeDocumentProcess(object[] args)
         {
-            if (args.Length > 1 && args[0] is int width && args[1] is int height) 
+            if (args.Length > 1 && args[0] is int width && args[1] is int height)
             {
                 ResizeDocument(width, height);
             }
@@ -142,4 +138,4 @@ namespace PixiEditor.Models.DataHolders
             DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(oldWidth, oldHeight, width, height));
         }
     }
-}
+}

+ 9 - 4
PixiEditor/Models/DataHolders/Document/Document.cs

@@ -141,6 +141,11 @@ namespace PixiEditor.Models.DataHolders
 
         public UndoManager UndoManager { get; set; }
 
+        public void RaisePropertyChange(string name)
+        {
+            RaisePropertyChanged(name);
+        }
+
         public void CenterViewport()
         {
             RecenterZoombox = false; // It's a trick to trigger change in UserControl
@@ -176,12 +181,12 @@ namespace PixiEditor.Models.DataHolders
             int oldHeight = Height;
 
             MoveOffsets(Layers, moveVector);
-            Width = width;
-            Height = height;
 
             object[] reverseArguments = { oldOffsets, oldWidth, oldHeight };
             object[] processArguments = { Layers.Select(x => x.Offset).ToArray(), width, height };
 
+            ResizeCanvasProcess(processArguments);
+
             UndoManager.AddUndoChange(new Change(
                 ResizeCanvasProcess,
                 reverseArguments,
@@ -195,8 +200,8 @@ namespace PixiEditor.Models.DataHolders
         /// </summary>
         public void CenterContent()
         {
-            var layersToCenter = Layers.Where(x => x.IsActive && x.IsVisible);
-            if (layersToCenter.Count() == 0)
+            var layersToCenter = Layers.Where(x => x.IsActive && GetFinalLayerIsVisible(x));
+            if (!layersToCenter.Any())
             {
                 return;
             }

+ 0 - 26
PixiEditor/Models/IO/BinarySerialization.cs

@@ -1,26 +0,0 @@
-using System.IO;
-using System.Runtime.Serialization.Formatters.Binary;
-
-namespace PixiEditor.Models.IO
-{
-    public static class BinarySerialization
-    {
-        public static void WriteToBinaryFile<T>(string path, T objectToWrite)
-        {
-            using (Stream stream = File.Open(path, FileMode.Create))
-            {
-                BinaryFormatter binaryFormatter = new BinaryFormatter();
-                binaryFormatter.Serialize(stream, objectToWrite);
-            }
-        }
-
-        public static T ReadFromBinaryFile<T>(string path)
-        {
-            using (Stream stream = File.Open(path, FileMode.Open))
-            {
-                BinaryFormatter formatter = new BinaryFormatter();
-                return (T)formatter.Deserialize(stream);
-            }
-        }
-    }
-}

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

@@ -6,7 +6,6 @@ using Microsoft.Win32;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
-using PixiEditor.Parser;
 
 namespace PixiEditor.Models.IO
 {
@@ -42,7 +41,7 @@ namespace PixiEditor.Models.IO
         /// <returns>Path.</returns>
         public static string SaveAsEditableFile(Document document, string path)
         {
-            PixiParser.Serialize(document.ToSerializable(), path);
+            Parser.PixiParser.Serialize(document.ToSerializable(), path);
             return path;
         }
 

+ 57 - 57
PixiEditor/Models/IO/Importer.cs

@@ -1,13 +1,13 @@
 using System;
-using System.IO;
-using System.Runtime.Serialization;
+using System.IO;
+using System.Runtime.Serialization;
 using System.Windows.Media.Imaging;
-using PixiEditor.Exceptions;
+using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
-using PixiEditor.Helpers.Extensions;
+using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.DataHolders;
-using PixiEditor.Parser;
-
+using PixiEditor.Parser;
+
 namespace PixiEditor.Models.IO
 {
     public class Importer : NotifyableObject
@@ -20,61 +20,61 @@ namespace PixiEditor.Models.IO
         /// <param name="height">New height of image.</param>
         /// <returns>WriteableBitmap of imported image.</returns>
         public static WriteableBitmap ImportImage(string path, int width, int height)
-        {
-            WriteableBitmap wbmp = ImportImage(path);
-            if (wbmp.PixelWidth != width || wbmp.PixelHeight != height)
-            {
-                return wbmp.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
-            }
-
-            return wbmp;
-        }
-
+        {
+            WriteableBitmap 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)
-        {
-            try
-            {
-                Uri uri = new Uri(path, UriKind.RelativeOrAbsolute);
-                BitmapImage bitmap = new BitmapImage();
-                bitmap.BeginInit();
-                bitmap.UriSource = uri;
-                bitmap.CacheOption = BitmapCacheOption.OnLoad;
-                bitmap.EndInit();
-
-                return BitmapFactory.ConvertToPbgra32Format(bitmap);
-            }
-            catch (NotSupportedException)
-            {
-                throw new CorruptedFileException();
-            }
-            catch (FileFormatException)
-            {
-                throw new CorruptedFileException();
-            }
-        }
-
-        public static Document ImportDocument(string path)
-        {
-            try
+        public static WriteableBitmap ImportImage(string path)
+        {
+            try
+            {
+                Uri uri = new Uri(path, UriKind.RelativeOrAbsolute);
+                BitmapImage bitmap = new BitmapImage();
+                bitmap.BeginInit();
+                bitmap.UriSource = uri;
+                bitmap.CacheOption = BitmapCacheOption.OnLoad;
+                bitmap.EndInit();
+
+                return BitmapFactory.ConvertToPbgra32Format(bitmap);
+            }
+            catch (NotSupportedException)
+            {
+                throw new CorruptedFileException();
+            }
+            catch (FileFormatException)
+            {
+                throw new CorruptedFileException();
+            }
+        }
+
+        public static Document ImportDocument(string path)
+        {
+            try
+            {
+                Document doc = PixiEditor.Parser.PixiParser.Deserialize(path).ToDocument();
+                doc.DocumentFilePath = path;
+                return doc;
+            }
+            catch (InvalidFileException)
             {
-                Document doc = PixiParser.Deserialize(path).ToDocument();
-                doc.DocumentFilePath = path;
-                return doc;
-            }
-            catch (InvalidFileException)
-            {
-                throw new CorruptedFileException();
-            }
-        }
-
-        public static bool IsSupportedFile(string path)
-        {
-            path = path.ToLower();
-            return path.EndsWith(".pixi") || path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg");
-        }
+                throw new CorruptedFileException();
+            }
+        }
+
+        public static bool IsSupportedFile(string path)
+        {
+            path = path.ToLower();
+            return path.EndsWith(".pixi") || path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg");
+        }
     }
 }

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

@@ -1,5 +1,6 @@
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
+using PixiEditor.Models.Layers.Utils;
 using PixiEditor.Models.Position;
 using PixiEditor.Parser;
 using System;
@@ -39,7 +40,7 @@ namespace PixiEditor.Models.ImageManipulation
         /// <param name="height">Height of final bitmap.</param>.
         /// <param name="layers">Layers to combine.</param>
         /// <returns>WriteableBitmap of layered bitmaps.</returns>
-        public static WriteableBitmap CombineLayers(int width, int height, params Layer[] layers)
+        public static WriteableBitmap CombineLayers(int width, int height, Layer[] layers, LayerStructure structure = null)
         {
             WriteableBitmap finalBitmap = BitmapFactory.New(width, height);
 
@@ -47,7 +48,7 @@ namespace PixiEditor.Models.ImageManipulation
             {
                 for (int i = 0; i < layers.Length; i++)
                 {
-                    float layerOpacity = layers[i].Opacity;
+                    float layerOpacity = structure == null ? layers[i].Opacity : LayerStructureUtils.GetFinalLayerOpacity(layers[i], structure);
                     Layer layer = layers[i];
 
                     if (layer.OffsetX < 0 || layer.OffsetY < 0 ||
@@ -109,6 +110,20 @@ namespace PixiEditor.Models.ImageManipulation
                 maxPreviewHeight);
         }
 
+        public static WriteableBitmap GeneratePreviewBitmap(IEnumerable<Layer> layers, int width, int height, int maxPreviewWidth, int maxPreviewHeight)
+        {
+            var opacityLayers = layers.Where(x => x.IsVisible && x.Opacity > 0.8f);
+
+            return GeneratePreviewBitmap(
+                opacityLayers.Select(x => x.LayerBitmap),
+                opacityLayers.Select(x => x.OffsetX),
+                opacityLayers.Select(x => x.OffsetY),
+                width,
+                height,
+                maxPreviewWidth,
+                maxPreviewHeight);
+        }
+
         public static WriteableBitmap GeneratePreviewBitmap(IEnumerable<SerializableLayer> layers, int width, int height, int maxPreviewWidth, int maxPreviewHeight)
         {
             var opacityLayers = layers.Where(x => x.IsVisible && x.Opacity > 0.8f);

+ 15 - 0
PixiEditor/Models/Layers/GroupChangedEventArgs.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+
+namespace PixiEditor.Models.Layers
+{
+    public class GroupChangedEventArgs : EventArgs
+    {
+        public List<GuidStructureItem> GroupsAffected { get; set; }
+
+        public GroupChangedEventArgs(List<GuidStructureItem> groupsAffected)
+        {
+            GroupsAffected = groupsAffected;
+        }
+    }
+}

+ 23 - 0
PixiEditor/Models/Layers/GroupData.cs

@@ -0,0 +1,23 @@
+using PixiEditor.Views.UserControls;
+using System;
+
+namespace PixiEditor.Models.Layers
+{
+    public record GroupData
+    {
+        public int TopIndex { get; set; }
+        public int BottomIndex { get; set; }
+        public Guid? GroupGuid { get; set; }
+
+        public GroupData(Guid? groupGuid)
+        {
+            GroupGuid = groupGuid;
+        }
+
+        public GroupData(int topIndex, int bottomIndex)
+        {
+            TopIndex = topIndex;
+            BottomIndex = bottomIndex;
+        }
+    }
+}

+ 164 - 0
PixiEditor/Models/Layers/GuidStructureItem.cs

@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Linq;
+using PixiEditor.Helpers;
+
+namespace PixiEditor.Models.Layers
+{
+    [DebuggerDisplay("{Name} - {GroupGuid}")]
+    public class GuidStructureItem : NotifyableObject, ICloneable
+    {
+        public event EventHandler<GroupChangedEventArgs> GroupsChanged;
+
+        public Guid GroupGuid { get; init; }
+
+        private string name;
+
+        public string Name
+        {
+            get => name;
+            set
+            {
+                name = value;
+                RaisePropertyChanged(nameof(Name));
+            }
+        }
+
+        private Guid startLayerGuid;
+
+        public Guid StartLayerGuid
+        {
+            get => startLayerGuid;
+            set
+            {
+                startLayerGuid = value;
+                RaisePropertyChanged(nameof(StartLayerGuid));
+            }
+        }
+
+        private Guid endLayerGuid;
+
+        public Guid EndLayerGuid
+        {
+            get => endLayerGuid;
+            set
+            {
+                endLayerGuid = value;
+                RaisePropertyChanged(nameof(EndLayerGuid));
+            }
+        }
+
+        public ObservableCollection<GuidStructureItem> Subgroups { get; set; } = new ObservableCollection<GuidStructureItem>();
+
+        public GuidStructureItem Parent { get; set; }
+
+        private bool isExpanded;
+
+        public bool IsExpanded
+        {
+            get => isExpanded;
+            set => SetProperty(ref isExpanded, value);
+        }
+
+        private bool isRenaming = false;
+
+        public bool IsRenaming
+        {
+            get => isRenaming;
+            set => SetProperty(ref isRenaming, value);
+        }
+
+        private bool isVisible = true;
+
+        public bool IsVisible
+        {
+            get => isVisible;
+            set => SetProperty(ref isVisible, value);
+        }
+
+        private float opacity = 1;
+
+        public float Opacity
+        {
+            get => opacity;
+            set => SetProperty(ref opacity, value);
+        }
+
+        public GuidStructureItem(
+            string name,
+            Guid startLayerGuid,
+            Guid endLayerGuid,
+            IEnumerable<GuidStructureItem> subgroups,
+            GuidStructureItem parent)
+        {
+            Name = name;
+            Subgroups = new ObservableCollection<GuidStructureItem>(subgroups);
+            GroupGuid = Guid.NewGuid();
+            Parent = parent;
+            StartLayerGuid = startLayerGuid;
+            EndLayerGuid = endLayerGuid;
+            Subgroups.CollectionChanged += Subgroups_CollectionChanged;
+        }
+
+        public GuidStructureItem(string name, Guid layer)
+        {
+            Name = name;
+            GroupGuid = Guid.NewGuid();
+            Parent = null;
+            StartLayerGuid = layer;
+            EndLayerGuid = layer;
+            Subgroups.CollectionChanged += Subgroups_CollectionChanged;
+        }
+
+        public override int GetHashCode()
+        {
+            HashCode hc = default;
+            hc.Add(GroupGuid);
+            hc.Add(EndLayerGuid);
+            hc.Add(StartLayerGuid);
+            hc.Add(Opacity);
+            hc.Add(IsVisible);
+            hc.Add(IsExpanded);
+            hc.Add(IsRenaming);
+            hc.Add(Parent);
+            hc.Add(Subgroups);
+            return hc.ToHashCode();
+        }
+
+        public GuidStructureItem CloneGroup()
+        {
+            GuidStructureItem item = new(Name, StartLayerGuid, EndLayerGuid, Array.Empty<GuidStructureItem>(), null)
+            {
+                GroupGuid = GroupGuid,
+                IsExpanded = isExpanded,
+                IsRenaming = isRenaming
+            };
+
+            if(Subgroups.Count > 0)
+            {
+                item.Subgroups = new ObservableCollection<GuidStructureItem>();
+                for (int i = 0; i < Subgroups.Count; i++)
+                {
+                    item.Subgroups.Add(Subgroups[i].CloneGroup());
+                    item.Subgroups[^1].Parent = item;
+                }
+            }
+
+            return item;
+        }
+
+        public object Clone()
+        {
+            return CloneGroup();
+        }
+
+        private void Subgroups_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+        {
+            var args = new GroupChangedEventArgs(e.NewItems != null ? e.NewItems.Cast<GuidStructureItem>().ToList() : new List<GuidStructureItem>());
+            GroupsChanged?.Invoke(this, args);
+            Parent?.GroupsChanged?.Invoke(this, args);
+        }
+    }
+}

+ 12 - 2
PixiEditor/Models/Layers/Layer.cs

@@ -154,9 +154,9 @@ namespace PixiEditor.Models.Layers
                 if (opacity != value)
                 {
                     opacity = value;
-                    RaisePropertyChanged(nameof(Opacity));
-                    RaisePropertyChanged(nameof(OpacityUndoTriggerable));
                 }
+                RaisePropertyChanged(nameof(Opacity));
+                RaisePropertyChanged(nameof(OpacityUndoTriggerable));
             }
         }
 
@@ -209,6 +209,11 @@ namespace PixiEditor.Models.Layers
             LayerGuid = newGuid;
         }
 
+        public IEnumerable<Layer> GetLayers()
+        {
+            return new Layer[] { this };
+        }
+
         /// <summary>
         ///     Returns clone of layer.
         /// </summary>
@@ -227,6 +232,11 @@ namespace PixiEditor.Models.Layers
             };
         }
 
+        public void RaisePropertyChange(string property)
+        {
+            RaisePropertyChanged(property);
+        }
+
         /// <summary>
         ///     Resizes bitmap with it's content using NearestNeighbor interpolation.
         /// </summary>

+ 123 - 0
PixiEditor/Models/Layers/LayerGroup.cs

@@ -0,0 +1,123 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using PixiEditor.Helpers;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Models.Layers
+{
+    public class LayerGroup : NotifyableObject
+    {
+        public Guid GroupGuid { get; init; }
+
+        public GuidStructureItem StructureData { get; init; }
+
+        public ObservableCollection<Layer> Layers { get; set; } = new ObservableCollection<Layer>();
+
+        public ObservableCollection<LayerGroup> Subfolders { get; set; } = new ObservableCollection<LayerGroup>();
+
+        public IEnumerable Items => BuildItems();
+
+        private IEnumerable BuildItems()
+        {
+            List<object> obj = new(Layers.Reverse());
+            foreach (var subfolder in Subfolders)
+            {
+                obj.Insert(Math.Clamp(subfolder.DisplayIndex - DisplayIndex, 0, obj.Count), subfolder);
+            }
+
+            obj.Reverse();
+
+            return obj;
+        }
+
+        private string name;
+
+        public string Name
+        {
+            get => name;
+            set
+            {
+                name = value;
+                RaisePropertyChanged(nameof(Name));
+            }
+        }
+
+        private bool isExpanded = false;
+
+        public bool IsExpanded
+        {
+            get => isExpanded;
+            set
+            {
+                isExpanded = value;
+                UpdateIsExpandedInDocument(value);
+                RaisePropertyChanged(nameof(IsExpanded));
+            }
+        }
+
+        private int displayIndex;
+
+        public int DisplayIndex
+        {
+            get => displayIndex;
+            set
+            {
+                displayIndex = value;
+                RaisePropertyChanged(nameof(DisplayIndex));
+            }
+        }
+
+        private int topIndex;
+
+        public int TopIndex
+        {
+            get => topIndex;
+            set
+            {
+                topIndex = value;
+                RaisePropertyChanged(nameof(TopIndex));
+            }
+        }
+
+        private bool isRenaming;
+
+        public bool IsRenaming
+        {
+            get => isRenaming;
+            set
+            {
+                SetProperty(ref isRenaming, value);
+            }
+        }
+
+        private void UpdateIsExpandedInDocument(bool value)
+        {
+            var folder = ViewModelMain.Current.BitmapManager.ActiveDocument.LayerStructure.GetGroupByGuid(GroupGuid);
+            if (folder != null)
+            {
+                folder.IsExpanded = value;
+            }
+        }
+
+        public LayerGroup(IEnumerable<Layer> layers, IEnumerable<LayerGroup> subfolders, string name,
+            int displayIndex, int topIndex, GuidStructureItem structureData)
+            : this(layers, subfolders, name, Guid.NewGuid(), displayIndex, topIndex, structureData)
+        {
+        }
+
+        public LayerGroup(IEnumerable<Layer> layers, IEnumerable<LayerGroup> subfolders, string name,
+            Guid guid, int displayIndex, int topIndex, GuidStructureItem structureData)
+        {
+            Layers = new ObservableCollection<Layer>(layers);
+            Subfolders = new ObservableCollection<LayerGroup>(subfolders);
+            Name = name;
+            GroupGuid = guid;
+            DisplayIndex = displayIndex;
+            TopIndex = topIndex;
+            StructureData = structureData;
+        }
+    }
+}

+ 1 - 1
PixiEditor/Models/Layers/LayerHelper.cs

@@ -70,7 +70,7 @@ namespace PixiEditor.Models.Layers
             int height = yCloser.Height + offsetY + yOther.Height;
 
             // Merge both layers into a bitmap
-            WriteableBitmap mergedBitmap = BitmapUtils.CombineLayers((int)documentsSize.X, (int)documentsSize.Y, thisLayer, otherLayer);
+            WriteableBitmap mergedBitmap = BitmapUtils.CombineLayers((int)documentsSize.X, (int)documentsSize.Y, new Layer[] { thisLayer, otherLayer });
             mergedBitmap = mergedBitmap.Crop(xCloser.OffsetX, yCloser.OffsetY, width, height);
 
             // Create the new layer with the merged bitmap

+ 724 - 0
PixiEditor/Models/Layers/LayerStructure.cs

@@ -0,0 +1,724 @@
+using PixiEditor.Models.DataHolders;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace PixiEditor.Models.Layers
+{
+    // Notice for further developemnt. Remember to expose only GroupData classes if you want to modify this LayerStructure groups
+    // LayerStructure should figure out the GuidStructureItem from its internal data. This will ensure that data will
+    // modify correctly.
+    // You should pass GuidStructureItem for operating on protected and private methods for faster data manipulation.
+
+    /// <summary>
+    /// Class containing layer groups structure and methods to operate on it.
+    /// </summary>
+    public class LayerStructure
+    {
+        public event EventHandler<LayerStructureChangedEventArgs> LayerStructureChanged;
+
+        public ObservableCollection<GuidStructureItem> Groups { get; set; }
+
+        private Document Owner { get; }
+
+        /// <summary>
+        /// Checks whenever group contains only single layer and none subgroups.
+        /// </summary>
+        /// <param name="layerGuid">Guid of layer to check.</param>
+        /// <param name="layerGroup">Group to check.</param>
+        /// <returns>True if group contains single layer (EndLayerGuid and StartLayerGuid == layerGuid) and none subgroups.</returns>
+        public static bool GroupContainsOnlyLayer(Guid layerGuid, GuidStructureItem layerGroup)
+        {
+            return layerGroup != null && layerGroup.Subgroups.Count == 0 && layerGroup.StartLayerGuid == layerGuid && layerGroup.EndLayerGuid == layerGuid;
+        }
+
+        /// <summary>
+        /// Deep clones groups.
+        /// </summary>
+        /// <param name="groups">Groups to clone.</param>
+        /// <returns>ObservableCollection with cloned groups.</returns>
+        public static ObservableCollection<GuidStructureItem> CloneGroups(ObservableCollection<GuidStructureItem> groups)
+        {
+            ObservableCollection<GuidStructureItem> outputGroups = new();
+            foreach (var group in groups.ToArray())
+            {
+                outputGroups.Add(group.CloneGroup());
+            }
+
+            return outputGroups;
+        }
+
+        /// <summary>
+        ///  Finds parent group by layer guid.
+        /// </summary>
+        /// <param name="layerGuid">Guid of group to check.</param>
+        /// <returns><see cref="GuidStructureItem"/>if parent group was found or null if not.</returns>
+        public GuidStructureItem GetGroupByLayer(Guid layerGuid)
+        {
+            return GetGroupByLayer(layerGuid, Groups);
+        }
+
+        /// <summary>
+        /// Finds <see cref="GuidStructureItem"/> (Group) by it's guid.
+        /// </summary>
+        /// <param name="groupGuid">Guid of group.</param>
+        /// <returns><see cref="GuidStructureItem"/> if group was found or null if not.</returns>
+        public GuidStructureItem GetGroupByGuid(Guid? groupGuid)
+        {
+            return GetGroupByGuid(groupGuid, Groups);
+        }
+
+        public ObservableCollection<GuidStructureItem> CloneGroups()
+        {
+            return CloneGroups(Groups);
+        }
+
+        // This will allow to add new group with multiple layers and groups at once. Not working well, todo fix
+        /*public GuidStructureItem AddNewGroup(string groupName, IEnumerable<Layer> layers, Guid activeLayer)
+        {
+            var activeLayerParent = GetGroupByLayer(activeLayer);
+
+            List<GuidStructureItem> sameLevelGroups = new List<GuidStructureItem>();
+
+            var group = AddNewGroup(groupName, activeLayer);
+
+            if (activeLayerParent == null)
+            {
+                sameLevelGroups.AddRange(Groups);
+            }
+            else
+            {
+                sameLevelGroups.AddRange(activeLayerParent.Subgroups);
+            }
+
+            sameLevelGroups.Remove(group);
+            group.Subgroups = new ObservableCollection<GuidStructureItem>(sameLevelGroups);
+
+            sameLevelGroups = new(sameLevelGroups.Where(x => IsChildOf(activeLayer, x)));
+
+            Guid lastLayer = activeLayer;
+
+            foreach (var layer in layers)
+            {
+                if (layer.LayerGuid == activeLayer)
+                {
+                    continue;
+                }
+
+                Owner.MoveLayerInStructure(layer.LayerGuid, lastLayer, false);
+                lastLayer = layer.LayerGuid;
+            }
+
+            return group;
+        }*/
+
+        /// <summary>
+        /// Adds a new group to layer structure taking into consideration nesting. Invokes LayerStructureChanged event.
+        /// </summary>
+        /// <param name="groupName">Name of a group.</param>
+        /// <param name="childLayer">Child layer of a new group.</param>
+        /// <returns>Newly created group (<see cref="GuidStructureItem"/>).</returns>
+        public GuidStructureItem AddNewGroup(string groupName, Guid childLayer)
+        {
+            var parent = GetGroupByLayer(childLayer);
+            GuidStructureItem group = new(groupName, childLayer) { IsExpanded = true };
+            if (parent == null)
+            {
+                Groups.Add(group);
+            }
+            else
+            {
+                group.Parent = parent;
+                parent.Subgroups.Add(group);
+            }
+
+            group.GroupsChanged += Group_GroupsChanged;
+
+            LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(childLayer));
+            return group;
+        }
+
+        public GuidStructureItem AddNewGroup(string groupName, GuidStructureItem childGroup)
+        {
+            if (childGroup == null)
+            {
+                throw new ArgumentException("Child group can't be null.");
+            }
+            GuidStructureItem group = new(groupName, childGroup.StartLayerGuid, childGroup.EndLayerGuid, new[] { childGroup }, childGroup.Parent) { IsExpanded = true };
+            if (childGroup.Parent == null)
+            {
+                Groups.Add(group);
+                Groups.Remove(childGroup);
+            }
+            else
+            {
+                childGroup.Parent.Subgroups.Add(group);
+                childGroup.Parent.Subgroups.Remove(childGroup);
+            }
+
+            childGroup.Parent = group;
+
+            group.GroupsChanged += Group_GroupsChanged;
+
+            LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(GetGroupLayerGuids(group)));
+            return group;
+        }
+
+        /// <summary>
+        /// Moves group and it's children from one index to another. This method makes changes in <see cref="Document"/> Layers.
+        /// </summary>
+        /// <param name="groupGuid">Group guid to move.</param>
+        /// <param name="newIndex">New group index, relative to <see cref="Document"/> Layers.</param>
+        public void MoveGroup(Guid groupGuid, int newIndex)
+        {
+            var group = GetGroupByGuid(groupGuid);
+            var parentGroup = group.Parent;
+            bool reverseOrder = true;
+            int groupTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == group.EndLayerGuid));
+            int groupBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == group.StartLayerGuid));
+
+            int difference = newIndex - groupTopIndex;
+
+            if (newIndex < groupTopIndex)
+            {
+                reverseOrder = false;
+                difference = newIndex - groupBottomIndex;
+            }
+
+            if (difference == 0)
+            {
+                return;
+            }
+
+            PreMoveReassignBounds(parentGroup, group);
+
+            List<Guid> layersInOrder = GetLayersInOrder(new GroupData(groupTopIndex, groupBottomIndex));
+
+            MoveLayersInGroup(layersInOrder, difference, reverseOrder);
+
+            LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(layersInOrder));
+        }
+
+        /// <summary>
+        /// Checks if group is nested inside parent group.
+        /// </summary>
+        /// <param name="group">Group to check.</param>
+        /// <param name="parent">Parent of that group.</param>
+        /// <returns>True if group is nested inside parent, false if not.</returns>
+        public bool IsChildOf(GuidStructureItem group, GuidStructureItem parent)
+        {
+            if (group == null)
+            {
+                return false;
+            }
+
+            foreach (var subgroup in parent.Subgroups)
+            {
+                if (subgroup == group)
+                {
+                    return true;
+                }
+
+                if (subgroup.Subgroups.Count > 0)
+                {
+                    if (IsChildOf(group, subgroup))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Checks if layer is nested inside parent group.
+        /// </summary>
+        /// <param name="layerGuid">Layer GUID to check.</param>
+        /// <param name="parent">Parent of that group.</param>
+        /// <returns>True if layer is nested inside parent, false if not.</returns>
+        public bool IsChildOf(Guid layerGuid, GuidStructureItem parent)
+        {
+            var layerParent = GetGroupByLayer(layerGuid);
+
+            return layerParent == parent ? true : IsChildOf(layerParent, parent);
+        }
+
+        /// <summary>
+        /// Reassigns (removes) group data from parent group.
+        /// </summary>
+        /// <param name="parentGroup">Parent group to reassign data in.</param>
+        /// <param name="group">Group which data should be reassigned.</param>
+        public void PreMoveReassignBounds(GroupData parentGroup, GroupData group)
+        {
+            PreMoveReassignBounds(GetGroupByGuid(parentGroup.GroupGuid), GetGroupByGuid(group.GroupGuid));
+        }
+
+        /// <summary>
+        /// Reassigns (removes) layer data from parent group.
+        /// </summary>
+        /// <param name="parentGroup">Parent group to reassign data in.</param>
+        /// <param name="layer">Layer which data should be reassigned.</param>
+        public void PreMoveReassignBounds(GroupData parentGroup, Guid layer)
+        {
+            PreMoveReassignBounds(GetGroupByGuid(parentGroup.GroupGuid), layer);
+        }
+
+        /// <summary>
+        /// Reassigns (adds) layer data to parent group.
+        /// </summary>
+        /// <param name="parentGroup">Parent group to reassign data in.</param>
+        /// <param name="layerGuid">Group which data should be reassigned.</param>
+        public void PostMoveReassignBounds(GroupData parentGroup, Guid layerGuid)
+        {
+            PostMoveReassignBounds(GetGroupByGuid(parentGroup.GroupGuid), layerGuid);
+        }
+
+        /// <summary>
+        /// Reassigns (adds) group data to parent group.
+        /// </summary>
+        /// <param name="parentGroup">Parent group to reassign data in.</param>
+        /// <param name="group">Group which data should be reassigned.</param>
+        public void PostMoveReassignBounds(GroupData parentGroup, GroupData group)
+        {
+            PostMoveReassignBounds(GetGroupByGuid(parentGroup?.GroupGuid), GetGroupByGuid(group.GroupGuid));
+        }
+
+        /// <summary>
+        /// Assigns parent to a layer.
+        /// </summary>
+        /// <param name="layer">Layer to assign parent to.</param>
+        /// <param name="parent">Parent which should be assigned. Null indicates no parent.</param>
+        public void AssignParent(Guid layer, Guid? parent)
+        {
+            AssignParent(layer, parent.HasValue ? GetGroupByGuid(parent) : null);
+        }
+
+        /// <summary>
+        /// Assigns group new parent.
+        /// </summary>
+        /// <param name="group">Group to assign parent</param>
+        /// <param name="referenceLayerGroup">Parent of group.</param>
+        public void ReassignParent(GuidStructureItem group, GuidStructureItem referenceLayerGroup)
+        {
+            group.Parent?.Subgroups.Remove(group);
+            if (Groups.Contains(group))
+            {
+                Groups.Remove(group);
+            }
+
+            if (referenceLayerGroup == null)
+            {
+                if (!Groups.Contains(group))
+                {
+                    Groups.Add(group);
+                    group.Parent = null;
+                }
+            }
+            else
+            {
+                referenceLayerGroup.Subgroups.Add(group);
+                group.Parent = referenceLayerGroup;
+            }
+
+            LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(GetGroupLayerGuids(group)));
+        }
+
+        /// <summary>
+        /// Gets all layers inside group, including nested groups.
+        /// </summary>
+        /// <param name="group">Group to get layers from.</param>
+        /// <returns>List of layers.</returns>
+        public List<Layer> GetGroupLayers(GuidStructureItem group)
+        {
+            List<Layer> layers = new();
+            var layerGuids = GetGroupLayerGuids(group);
+            foreach (var layerGuid in layerGuids)
+            {
+                layers.Add(Owner.Layers.First(x => x.LayerGuid == layerGuid));
+            }
+
+            return layers;
+        }
+
+        /// <summary>
+        /// Sets parent groups IsExpanded to true.
+        /// </summary>
+        /// <param name="layerGuid">Guid of layer which parents will be affected.</param>
+        public void ExpandParentGroups(Guid layerGuid)
+        {
+            var group = GetGroupByLayer(layerGuid);
+
+            while (group != null)
+            {
+                group.IsExpanded = true;
+                group = group.Parent;
+            }
+        }
+
+        /// <summary>
+        /// Sets parent groups IsExpanded to true.
+        /// </summary>
+        /// <param name="group">Group which parents will be affected.</param>
+        public void ExpandParentGroups(GuidStructureItem group)
+        {
+            GuidStructureItem currentGroup = group;
+
+            while (currentGroup != null)
+            {
+                currentGroup.IsExpanded = true;
+                currentGroup = currentGroup.Parent;
+            }
+        }
+
+        private static Guid GetNextLayerGuid(Guid layer, List<Guid> allLayers, bool above)
+        {
+            int indexOfLayer = allLayers.IndexOf(layer);
+
+            int modifier = above ? 1 : -1;
+
+            int newIndex = Math.Clamp(indexOfLayer + modifier, 0, allLayers.Count - 1);
+
+            return allLayers[newIndex];
+        }
+
+        /// <summary>
+        /// Gets all layers inside group, including nested groups.
+        /// </summary>
+        /// <param name="group">Group to get layers from.</param>
+        /// <returns>List of layer guids.</returns>
+        private List<Guid> GetGroupLayerGuids(GuidStructureItem group)
+        {
+            Layer layerTop = Owner.Layers.FirstOrDefault(x => x.LayerGuid == group.EndLayerGuid);
+            Layer layerBottom = Owner.Layers.FirstOrDefault(x => x.LayerGuid == group.StartLayerGuid);
+
+            if (layerTop == null || layerBottom == null)
+            {
+                return new List<Guid>();
+            }
+
+            int indexTop = Owner.Layers.IndexOf(layerTop);
+            int indexBottom = Owner.Layers.IndexOf(layerBottom);
+
+            return GetLayersInOrder(new GroupData(indexTop, indexBottom));
+        }
+
+        private List<Guid> GetLayersInOrder(GroupData group)
+        {
+            List<Guid> layerGuids = new();
+            int minIndex = group.BottomIndex;
+            int maxIndex = group.TopIndex;
+
+            for (int i = minIndex; i <= maxIndex; i++)
+            {
+                layerGuids.Add(Owner.Layers[i].LayerGuid);
+            }
+
+            return layerGuids;
+        }
+
+        private void PreMoveReassignBounds(GuidStructureItem parentGroup, Guid layer)
+        {
+            if (parentGroup != null)
+            {
+                Guid oldStart = parentGroup.StartLayerGuid;
+                Guid oldEnd = parentGroup.EndLayerGuid;
+                GuidStructureItem parentOfParent = parentGroup.Parent;
+                if (parentGroup.Subgroups.Count == 0 && parentGroup.StartLayerGuid == layer && parentGroup.EndLayerGuid == layer)
+                {
+                    RemoveGroup(parentGroup);
+                }
+                else
+                {
+                    if (parentGroup.EndLayerGuid == layer)
+                    {
+                        parentGroup.EndLayerGuid = FindBoundLayer(parentGroup, layer, false);
+                    }
+
+                    if (parentGroup.StartLayerGuid == layer)
+                    {
+                        parentGroup.StartLayerGuid = FindBoundLayer(parentGroup, layer, true);
+                    }
+                }
+
+                if (parentOfParent != null)
+                {
+                    ApplyBoundsToParents(parentOfParent, parentGroup, oldStart, oldEnd);
+                }
+                LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(layer));
+            }
+        }
+
+        private void PreMoveReassignBounds(GuidStructureItem parentGroup, GuidStructureItem group)
+        {
+            if (parentGroup != null)
+            {
+                Guid oldStart = parentGroup.StartLayerGuid;
+                Guid oldEnd = parentGroup.EndLayerGuid;
+
+                if (parentGroup.Subgroups.Count == 1 && parentGroup.StartLayerGuid == group.StartLayerGuid && parentGroup.EndLayerGuid == group.EndLayerGuid)
+                {
+                    RemoveGroup(parentGroup);
+                }
+                else
+                {
+                    if (group.EndLayerGuid == parentGroup.EndLayerGuid)
+                    {
+                        parentGroup.EndLayerGuid = FindBoundLayer(parentGroup, group.StartLayerGuid, false);
+                    }
+
+                    if (group.StartLayerGuid == parentGroup.StartLayerGuid)
+                    {
+                        parentGroup.StartLayerGuid = FindBoundLayer(parentGroup, group.EndLayerGuid, true);
+                    }
+                }
+
+                if (parentGroup.Parent != null)
+                {
+                    ApplyBoundsToParents(parentGroup.Parent, parentGroup, oldStart, oldEnd);
+                }
+            }
+        }
+
+        private void PostMoveReassignBounds(GuidStructureItem parentGroup, Guid layerGuid)
+        {
+            if (parentGroup != null)
+            {
+                Guid? oldStart = parentGroup.StartLayerGuid;
+                Guid? oldEnd = parentGroup.EndLayerGuid;
+
+                int layerIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == layerGuid));
+
+                int folderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentGroup.EndLayerGuid));
+                int folderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentGroup.StartLayerGuid));
+
+                int finalTopIndex = Math.Max(folderTopIndex, layerIndex);
+                int finalBottomIndex = Math.Min(folderBottomIndex, layerIndex);
+
+                Guid? topBoundLayer = FindBoundLayer(layerGuid, finalTopIndex, finalBottomIndex, false);
+                Guid? bottomBoundLayer = FindBoundLayer(layerGuid, finalTopIndex, finalBottomIndex, true);
+
+                if (topBoundLayer == parentGroup.EndLayerGuid)
+                {
+                    parentGroup.EndLayerGuid = layerGuid;
+                }
+
+                if (bottomBoundLayer == parentGroup.StartLayerGuid)
+                {
+                    parentGroup.StartLayerGuid = layerGuid;
+                }
+
+                if (parentGroup.Parent != null)
+                {
+                    ApplyBoundsToParents(parentGroup.Parent, parentGroup, oldStart, oldEnd);
+                }
+
+                var args = new LayerStructureChangedEventArgs(layerGuid);
+
+                if (topBoundLayer.HasValue)
+                {
+                    args.AffectedLayerGuids.Add(topBoundLayer.Value);
+                }
+                if (bottomBoundLayer.HasValue)
+                {
+                    args.AffectedLayerGuids.Add(bottomBoundLayer.Value);
+                }
+
+                LayerStructureChanged?.Invoke(this, args);
+            }
+        }
+
+        private void PostMoveReassignBounds(GuidStructureItem parentGroup, GuidStructureItem group)
+        {
+            if (parentGroup != null)
+            {
+                Guid oldStart = parentGroup.StartLayerGuid;
+                Guid oldEnd = parentGroup.EndLayerGuid;
+                int folderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == group.EndLayerGuid));
+                int folderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == group.StartLayerGuid));
+
+                int parentFolderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentGroup.EndLayerGuid));
+                int parentFolderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentGroup.StartLayerGuid));
+
+                int finalTopIndex = Math.Max(folderTopIndex, parentFolderTopIndex);
+                int finalBottomIndex = Math.Min(folderBottomIndex, parentFolderBottomIndex);
+
+                Guid topBoundLayer = FindBoundLayer(group.StartLayerGuid, finalTopIndex, finalBottomIndex, false);
+                Guid bottomBoundLayer = FindBoundLayer(group.EndLayerGuid, finalTopIndex, finalBottomIndex, true);
+
+                if (topBoundLayer == parentGroup.EndLayerGuid)
+                {
+                    parentGroup.EndLayerGuid = group.EndLayerGuid;
+                }
+
+                if (bottomBoundLayer == parentGroup.StartLayerGuid)
+                {
+                    parentGroup.StartLayerGuid = group.StartLayerGuid;
+                }
+
+                if (parentGroup.Parent != null)
+                {
+                    ApplyBoundsToParents(parentGroup.Parent, parentGroup, oldStart, oldEnd);
+                }
+            }
+        }
+
+        private void AssignParent(Guid layer, GuidStructureItem parent)
+        {
+            var currentParent = GetGroupByLayer(layer);
+            if (currentParent != null)
+            {
+                PreMoveReassignBounds(currentParent, layer);
+            }
+
+            PostMoveReassignBounds(parent, layer);
+
+            LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(layer));
+        }
+
+        private void Group_GroupsChanged(object sender, GroupChangedEventArgs e)
+        {
+            List<Guid> layersAffected = new List<Guid>();
+            e.GroupsAffected.ForEach(x => layersAffected.AddRange(GetGroupLayerGuids(x)));
+            LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(layersAffected));
+        }
+
+        private void RemoveGroup(GuidStructureItem parentFolder)
+        {
+            parentFolder.GroupsChanged -= Group_GroupsChanged;
+
+            var layerGuids = GetGroupLayerGuids(parentFolder);
+
+            if (parentFolder.Parent == null)
+            {
+                Groups.Remove(parentFolder);
+            }
+            else
+            {
+                parentFolder.Parent.Subgroups.Remove(parentFolder);
+            }
+
+            LayerStructureChanged?.Invoke(this, new LayerStructureChangedEventArgs(layerGuids));
+
+        }
+
+        private void ApplyBoundsToParents(GuidStructureItem parent, GuidStructureItem group, Guid? oldStart, Guid? oldEnd)
+        {
+            Guid parentOldStart = parent.StartLayerGuid;
+            Guid parentOldEnd = parent.EndLayerGuid;
+
+            if (parent.Subgroups.Count == 0)
+            {
+                RemoveGroup(parent);
+            }
+
+            if (parent.StartLayerGuid == oldStart)
+            {
+                parent.StartLayerGuid = group.StartLayerGuid;
+            }
+
+            if (parent.EndLayerGuid == oldEnd)
+            {
+                parent.EndLayerGuid = group.EndLayerGuid;
+            }
+
+            if (parent.Parent != null)
+            {
+                ApplyBoundsToParents(parent.Parent, parent, parentOldStart, parentOldEnd);
+            }
+        }
+
+        private Guid FindBoundLayer(Guid layerGuid, int parentFolderTopIndex, int parentFolderBottomIndex, bool above)
+        {
+            return GetNextLayerGuid(
+                   layerGuid,
+                   GetLayersInOrder(new GroupData(parentFolderTopIndex, parentFolderBottomIndex)),
+                   above);
+        }
+
+        private Guid FindBoundLayer(GuidStructureItem parentFolder, Guid layerGuid, bool above)
+        {
+            int parentFolderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentFolder.EndLayerGuid));
+            int parentFolderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentFolder.StartLayerGuid));
+
+            return FindBoundLayer(layerGuid, parentFolderTopIndex, parentFolderBottomIndex, above);
+        }
+
+        private void MoveLayersInGroup(List<Guid> layers, int moveBy, bool reverseOrder)
+        {
+            List<Guid> layerGuids = reverseOrder ? layers.Reverse<Guid>().ToList() : layers;
+
+            for (int i = 0; i < layers.Count; i++)
+            {
+                Guid layerGuid = layerGuids[i];
+                var layer = Owner.Layers.First(x => x.LayerGuid == layerGuid);
+                int layerIndex = Owner.Layers.IndexOf(layer);
+                Owner.Layers.Move(layerIndex, layerIndex + moveBy);
+            }
+        }
+
+        private GuidStructureItem GetGroupByLayer(Guid layerGuid, IEnumerable<GuidStructureItem> groups)
+        {
+            foreach (var currentGroup in groups)
+            {
+                var endLayer = Owner.Layers.First(x => x.LayerGuid == currentGroup.EndLayerGuid);
+                var startLayer = Owner.Layers.First(x => x.LayerGuid == currentGroup.StartLayerGuid);
+
+                int topIndex = Owner.Layers.IndexOf(endLayer);
+                int bottomIndex = Owner.Layers.IndexOf(startLayer);
+                var layers = GetLayersInOrder(new GroupData(topIndex, bottomIndex));
+
+                if (currentGroup.Subgroups.Count > 0)
+                {
+                    var group = GetGroupByLayer(layerGuid, currentGroup.Subgroups);
+                    if (group != null)
+                    {
+                        return group;
+                    }
+                }
+
+                if (layers.Contains(layerGuid))
+                {
+                    return currentGroup;
+                }
+            }
+
+            return null;
+        }
+
+        private GuidStructureItem GetGroupByGuid(Guid? groupGuid, IEnumerable<GuidStructureItem> groups)
+        {
+            foreach (var group in groups)
+            {
+                if (group.GroupGuid == groupGuid)
+                {
+                    return group;
+                }
+
+                if (group.Subgroups.Count > 0)
+                {
+                    var guid = GetGroupByGuid(groupGuid, group.Subgroups);
+                    if (guid != null)
+                    {
+                        return guid;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        public LayerStructure(ObservableCollection<GuidStructureItem> items, Document owner)
+        {
+            Groups = items;
+            Owner = owner;
+        }
+
+        public LayerStructure(Document owner)
+        {
+            Groups = new ObservableCollection<GuidStructureItem>();
+            Owner = owner;
+        }
+    }
+}

+ 20 - 0
PixiEditor/Models/Layers/LayerStructureChangedEventArgs.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+
+namespace PixiEditor.Models.Layers
+{
+    public class LayerStructureChangedEventArgs : EventArgs
+    {
+        public List<Guid> AffectedLayerGuids { get; set; }
+
+        public LayerStructureChangedEventArgs(List<Guid> affectedLayerGuids)
+        {
+            AffectedLayerGuids = affectedLayerGuids;
+        }
+
+        public LayerStructureChangedEventArgs(Guid affectedLayerGuid)
+        {
+            AffectedLayerGuids = new List<Guid>() { affectedLayerGuid };
+        }
+    }
+}

+ 203 - 0
PixiEditor/Models/Layers/StructuredLayerTree.cs

@@ -0,0 +1,203 @@
+using PixiEditor.Helpers;
+using PixiEditor.Helpers.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace PixiEditor.Models.Layers
+{
+    public class StructuredLayerTree : NotifyableObject
+    {
+        private List<Guid> layersInStructure = new();
+
+        public ObservableCollection<object> RootDirectoryItems { get; } = new ObservableCollection<object>();
+
+        private static void Swap(ref int startIndex, ref int endIndex)
+        {
+            int tmp = startIndex;
+            startIndex = endIndex;
+            endIndex = tmp;
+        }
+
+        public StructuredLayerTree(ObservableCollection<Layer> layers, LayerStructure structure)
+        {
+            if (layers == null || structure == null)
+            {
+                return;
+            }
+
+            if (structure.Groups == null || structure.Groups.Count == 0)
+            {
+                RootDirectoryItems.AddRange(layers);
+                return;
+            }
+
+            var parsedFolders = ParseFolders(structure.Groups, layers);
+
+            parsedFolders = parsedFolders.OrderBy(x => x.DisplayIndex).ToList();
+
+            PlaceItems(parsedFolders, layers);
+
+            layersInStructure.Clear();
+        }
+
+        private void PlaceItems(List<LayerGroup> parsedFolders, ObservableCollection<Layer> layers)
+        {
+            LayerGroup currentFolder = null;
+            List<LayerGroup> groupsAtIndex = new();
+            Stack<LayerGroup> unfinishedFolders = new();
+
+            for (int i = 0; i < layers.Count; i++)
+            {
+                if (currentFolder != null && layers[i].LayerGuid == currentFolder.StructureData.EndLayerGuid)
+                {
+                    if (unfinishedFolders.Count > 0)
+                    {
+                        currentFolder = unfinishedFolders.Pop();
+                    }
+                    else
+                    {
+                        currentFolder = null;
+                    }
+
+                    continue;
+                }
+
+                AssignGroup(parsedFolders, layers, ref currentFolder, ref groupsAtIndex, unfinishedFolders, i);
+
+                if (currentFolder == null && !layersInStructure.Contains(layers[i].LayerGuid))
+                {
+                    RootDirectoryItems.Add(layers[i]);
+                }
+                else if (!RootDirectoryItems.Contains(currentFolder))
+                {
+                    RootDirectoryItems.AddRange(groupsAtIndex.Where(x => !RootDirectoryItems.Contains(x)));
+                }
+            }
+        }
+
+        private void AssignGroup(List<LayerGroup> parsedFolders, ObservableCollection<Layer> layers, ref LayerGroup currentFolder, ref List<LayerGroup> groupsAtIndex, Stack<LayerGroup> unfinishedFolders, int i)
+        {
+            if (parsedFolders.Any(x => x.StructureData.StartLayerGuid == layers[i].LayerGuid))
+            {
+                groupsAtIndex = parsedFolders.Where(x => x.StructureData.StartLayerGuid == layers[i].LayerGuid).ToList();
+                for (int j = 0; j < groupsAtIndex.Count; j++)
+                {
+                    LayerGroup group = groupsAtIndex[j];
+
+                    if (currentFolder != null)
+                    {
+                        unfinishedFolders.Push(currentFolder);
+                    }
+
+                    groupsAtIndex[j] = parsedFolders.First(x => x.StructureData.StartLayerGuid == layers[i].LayerGuid);
+                    groupsAtIndex[j].DisplayIndex = RootDirectoryItems.Count;
+                    groupsAtIndex[j].TopIndex = CalculateTopIndex(group.DisplayIndex, group.StructureData, layers);
+                    if (groupsAtIndex[j].StructureData.EndLayerGuid != layers[i].LayerGuid)
+                    {
+                        currentFolder = groupsAtIndex[j];
+                    }
+                }
+            }
+        }
+
+        private int CalculateTopIndex(int displayIndex, GuidStructureItem structureData, ObservableCollection<Layer> layers)
+        {
+            var endLayer = layers.FirstOrDefault(x => x.LayerGuid == structureData.EndLayerGuid);
+            var bottomLayer = layers.FirstOrDefault(x => x.LayerGuid == structureData.StartLayerGuid);
+            int originalTopIndex = 0;
+            int originalBottomIndex = 0;
+            if (endLayer != null)
+            {
+                originalTopIndex = layers.IndexOf(endLayer);
+            }
+            if (bottomLayer != null)
+            {
+                originalBottomIndex = layers.IndexOf(bottomLayer);
+            }
+
+            return displayIndex + (originalTopIndex - originalBottomIndex);
+        }
+
+        private List<LayerGroup> ParseFolders(IEnumerable<GuidStructureItem> folders, ObservableCollection<Layer> layers)
+        {
+            List<LayerGroup> parsedFolders = new();
+            foreach (var structureItem in folders)
+            {
+                parsedFolders.Add(ParseFolder(structureItem, layers));
+            }
+
+            return parsedFolders;
+        }
+
+        private LayerGroup ParseFolder(GuidStructureItem structureItem, ObservableCollection<Layer> layers)
+        {
+            List<Layer> structureItemLayers = new();
+
+            Guid[] layersInFolder = GetLayersInGroup(layers, structureItem);
+
+            var subFolders = new List<LayerGroup>();
+
+            if (structureItem.Subgroups.Count > 0)
+            {
+                subFolders = ParseFolders(structureItem.Subgroups, layers);
+            }
+
+            foreach (var guid in layersInFolder)
+            {
+                var layer = layers.FirstOrDefault(x => x.LayerGuid == guid);
+                if (layer != null)
+                {
+                    if (!layersInStructure.Contains(layer.LayerGuid))
+                    {
+                        layersInStructure.Add(layer.LayerGuid);
+                        structureItemLayers.Add(layer);
+                    }
+                }
+            }
+
+            int displayIndex = layersInFolder.Length > 0 ? layers.IndexOf(layers.First(x => x.LayerGuid == structureItem.StartLayerGuid)) : 0;
+
+            structureItemLayers.Reverse();
+
+            LayerGroup folder = new(structureItemLayers, subFolders, structureItem.Name,
+                structureItem.GroupGuid, displayIndex, displayIndex + structureItemLayers.Count - 1, structureItem)
+            {
+                IsExpanded = structureItem.IsExpanded,
+                IsRenaming = structureItem.IsRenaming
+            };
+            return folder;
+        }
+
+        private Guid[] GetLayersInGroup(ObservableCollection<Layer> layers, GuidStructureItem structureItem)
+        {
+            var startLayer = layers.FirstOrDefault(x => x.LayerGuid == structureItem.StartLayerGuid);
+            var endLayer = layers.FirstOrDefault(x => x.LayerGuid == structureItem.EndLayerGuid);
+
+            if (startLayer == null || endLayer == null)
+            {
+                return Array.Empty<Guid>();
+            }
+
+            int startIndex = layers.IndexOf(startLayer);
+            int endIndex = layers.IndexOf(endLayer);
+
+            if (startIndex > endIndex)
+            {
+                Swap(ref startIndex, ref endIndex);
+            }
+
+            int len = endIndex - startIndex + 1;
+
+            Guid[] guids = new Guid[len];
+
+            for (int i = 0; i < len; i++)
+            {
+                guids[i] = layers[i + startIndex].LayerGuid;
+            }
+
+            return guids;
+        }
+    }
+}

+ 36 - 0
PixiEditor/Models/Layers/Utils/LayerStructureUtils.cs

@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.Layers.Utils
+{
+    public static class LayerStructureUtils
+    {
+        /// <summary>
+        /// Gets final layer opacity taking into consideration parent groups.
+        /// </summary>
+        /// <param name="layer">Layer to check.</param>
+        /// <returns>Float from range 0-1.</returns>
+        public static float GetFinalLayerOpacity(Layer layer, LayerStructure structure)
+        {
+            if (layer.Opacity == 0)
+            {
+                return 0f;
+            }
+
+            var group = structure.GetGroupByLayer(layer.LayerGuid);
+            GuidStructureItem groupToCheck = group;
+            float finalOpacity = layer.Opacity;
+
+            while (groupToCheck != null)
+            {
+                finalOpacity *= groupToCheck.Opacity;
+                groupToCheck = groupToCheck.Parent;
+            }
+
+            return Math.Clamp(finalOpacity, 0f, 1f);
+        }
+    }
+}

+ 7 - 10
PixiEditor/Models/Position/CoordinatesCalculator.cs

@@ -123,40 +123,37 @@ namespace PixiEditor.Models.Position
         public static int FindMaxYNonTransparent(WriteableBitmap bitmap)
         {
             Color transparent = Color.FromArgb(0, 0, 0, 0);
-            bitmap.Lock();
-            for (int y = (int)bitmap.Height - 1; y >= 0; y--)
+            using BitmapContext ctx = bitmap.GetBitmapContext(ReadWriteMode.ReadOnly);
+            for (int y = ctx.Height - 1; y >= 0; y--)
             {
-                for (int x = (int)bitmap.Width - 1; x >= 0; x--)
+                for (int x = ctx.Width - 1; x >= 0; x--)
                 {
                     if (bitmap.GetPixel(x, y) != transparent)
                     {
-                        bitmap.Unlock();
                         return y;
                     }
                 }
             }
 
-            bitmap.Unlock();
             return -1;
         }
 
         public static int FindMaxXNonTransparent(WriteableBitmap bitmap)
         {
             Color transparent = Color.FromArgb(0, 0, 0, 0);
-            bitmap.Lock();
-            for (int x = (int)bitmap.Width - 1; x >= 0; x--)
+
+            using BitmapContext ctx = bitmap.GetBitmapContext(ReadWriteMode.ReadOnly);
+            for (int x = ctx.Width - 1; x >= 0; x--)
             {
-                for (int y = (int)bitmap.Height - 1; y >= 0; y--)
+                for (int y = ctx.Height - 1; y >= 0; y--)
                 {
                     if (bitmap.GetPixel(x, y) != transparent)
                     {
-                        bitmap.Unlock();
                         return x;
                     }
                 }
             }
 
-            bitmap.Unlock();
             return -1;
         }
     }

+ 3 - 1
PixiEditor/Models/Tools/BitmapOperationTool.cs

@@ -1,4 +1,6 @@
 using System;
+using System.Collections.Generic;
+using System.Windows.Documents;
 using System.Windows.Media;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
@@ -16,7 +18,7 @@ namespace PixiEditor.Models.Tools
 
         private readonly LayerChange[] onlyLayerArr = new LayerChange[] { new LayerChange(BitmapPixelChanges.Empty, Guid.Empty) };
 
-        public abstract LayerChange[] Use(Layer layer, Coordinates[] mouseMove, Color color);
+        public abstract LayerChange[] Use(Layer layer, List<Coordinates> mouseMove, Color color);
 
         protected LayerChange[] Only(BitmapPixelChanges changes, Layer layer)
         {

+ 3 - 2
PixiEditor/Models/Tools/ReadonlyTool.cs

@@ -1,9 +1,10 @@
-using PixiEditor.Models.Position;
+using System.Collections.Generic;
+using PixiEditor.Models.Position;
 
 namespace PixiEditor.Models.Tools
 {
     public abstract class ReadonlyTool : Tool
     {
-        public abstract void Use(Coordinates[] pixels);
+        public abstract void Use(List<Coordinates> pixels);
     }
 }

+ 43 - 43
PixiEditor/Models/Tools/ShapeTool.cs

@@ -1,74 +1,74 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Windows.Input;
-using System.Windows.Media;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Input;
+using System.Windows.Media;
 
 namespace PixiEditor.Models.Tools
 {
     public abstract class ShapeTool : BitmapOperationTool
     {
-        public ShapeTool()
-        {
-            RequiresPreviewLayer = true;
-            Cursor = Cursors.Cross;
-            Toolbar = new BasicShapeToolbar();
-        }
-
-        public static DoubleCords CalculateCoordinatesForShapeRotation(
+        public static DoubleCords CalculateCoordinatesForShapeRotation(
             Coordinates startingCords,
             Coordinates secondCoordinates)
         {
             Coordinates currentCoordinates = secondCoordinates;
 
-            if (startingCords.X > currentCoordinates.X && startingCords.Y > currentCoordinates.Y)
-            {
-                return new DoubleCords(
+            if (startingCords.X > currentCoordinates.X && startingCords.Y > currentCoordinates.Y)
+            {
+                return new DoubleCords(
                     new Coordinates(currentCoordinates.X, currentCoordinates.Y),
-                    new Coordinates(startingCords.X, startingCords.Y));
-            }
-
-            if (startingCords.X < currentCoordinates.X && startingCords.Y < currentCoordinates.Y)
-            {
-                return new DoubleCords(
+                    new Coordinates(startingCords.X, startingCords.Y));
+            }
+
+            if (startingCords.X < currentCoordinates.X && startingCords.Y < currentCoordinates.Y)
+            {
+                return new DoubleCords(
                     new Coordinates(startingCords.X, startingCords.Y),
-                    new Coordinates(currentCoordinates.X, currentCoordinates.Y));
-            }
-
-            if (startingCords.Y > currentCoordinates.Y)
-            {
-                return new DoubleCords(
+                    new Coordinates(currentCoordinates.X, currentCoordinates.Y));
+            }
+
+            if (startingCords.Y > currentCoordinates.Y)
+            {
+                return new DoubleCords(
                     new Coordinates(startingCords.X, currentCoordinates.Y),
-                    new Coordinates(currentCoordinates.X, startingCords.Y));
-            }
-
-            if (startingCords.X > currentCoordinates.X && startingCords.Y <= currentCoordinates.Y)
-            {
-                return new DoubleCords(
+                    new Coordinates(currentCoordinates.X, startingCords.Y));
+            }
+
+            if (startingCords.X > currentCoordinates.X && startingCords.Y <= currentCoordinates.Y)
+            {
+                return new DoubleCords(
                     new Coordinates(currentCoordinates.X, startingCords.Y),
-                    new Coordinates(startingCords.X, currentCoordinates.Y));
-            }
-
+                    new Coordinates(startingCords.X, currentCoordinates.Y));
+            }
+
             return new DoubleCords(startingCords, secondCoordinates);
         }
 
+        public ShapeTool()
+        {
+            RequiresPreviewLayer = true;
+            Cursor = Cursors.Cross;
+            Toolbar = new BasicShapeToolbar();
+        }
+
         // TODO: Add cache for lines 31, 32 (hopefully it would speed up calculation)
-        public abstract override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color);
+        public abstract override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color);
 
         protected IEnumerable<Coordinates> GetThickShape(IEnumerable<Coordinates> shape, int thickness)
         {
             List<Coordinates> output = new List<Coordinates>();
-            foreach (Coordinates item in shape)
-            {
+            foreach (Coordinates item in shape)
+            {
                 output.AddRange(
                     CoordinatesCalculator.RectangleToCoordinates(
-                        CoordinatesCalculator.CalculateThicknessCenter(item, thickness)));
-            }
-
+                        CoordinatesCalculator.CalculateThicknessCenter(item, thickness)));
+            }
+
             return output.Distinct();
         }
     }
-}
+}

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

@@ -49,7 +49,7 @@ namespace PixiEditor.Models.Tools.Tools
             }
         }
 
-        public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
+        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
         {
             int toolSize = Toolbar.GetSetting<SizeSetting>("ToolSize").Value;
             float correctionFactor = Toolbar.GetSetting<FloatSetting>("CorrectionFactor").Value;

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

@@ -35,7 +35,7 @@ namespace PixiEditor.Models.Tools.Tools
             }
         }
 
-        public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
+        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
         {
             int thickness = Toolbar.GetSetting<SizeSetting>("ToolSize").Value;
             DoubleCords fixedCoordinates = CalculateCoordinatesForShapeRotation(coordinates[^1], coordinates[0]);

+ 3 - 2
PixiEditor/Models/Tools/Tools/ColorPickerTool.cs

@@ -1,4 +1,5 @@
-using System.Drawing;
+using System.Collections.Generic;
+using System.Drawing;
 using System.Windows.Input;
 using PixiEditor.Models.Position;
 using PixiEditor.ViewModels;
@@ -21,7 +22,7 @@ namespace PixiEditor.Models.Tools.Tools
             ViewModelMain.Current.ColorsSubViewModel.PrimaryColor = GetColorUnderMouse();
         }
 
-        public override void Use(Coordinates[] coordinates)
+        public override void Use(List<Coordinates> coordinates)
         {
             ViewModelMain.Current.ColorsSubViewModel.PrimaryColor = GetColorUnderMouse();
         }

+ 5 - 4
PixiEditor/Models/Tools/Tools/EraserTool.cs

@@ -1,4 +1,5 @@
-using System.Windows.Media;
+using System.Collections.Generic;
+using System.Windows.Media;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
@@ -18,14 +19,14 @@ namespace PixiEditor.Models.Tools.Tools
             Toolbar = new BasicToolbar();
         }
 
-        public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
+        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
         {
             return Erase(layer, coordinates, Toolbar.GetSetting<SizeSetting>("ToolSize").Value);
         }
 
-        public LayerChange[] Erase(Layer layer, Coordinates[] coordinates, int toolSize)
+        public LayerChange[] Erase(Layer layer, List<Coordinates> coordinates, int toolSize)
         {
-            Coordinates startingCords = coordinates.Length > 1 ? coordinates[1] : coordinates[0];
+            Coordinates startingCords = coordinates.Count > 1 ? coordinates[1] : coordinates[0];
             BitmapPixelChanges pixels = pen.Draw(startingCords, coordinates[0], System.Windows.Media.Colors.Transparent, toolSize);
             return Only(pixels, layer);
         }

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

@@ -15,7 +15,7 @@ namespace PixiEditor.Models.Tools.Tools
             Tooltip = "Fills area with color. (G)";
         }
 
-        public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
+        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
         {
             return Only(ForestFire(layer, coordinates[0], color), layer);
         }

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

@@ -39,7 +39,7 @@ namespace PixiEditor.Models.Tools.Tools
             }
         }
 
-        public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
+        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
         {
             BitmapPixelChanges pixels =
                 BitmapPixelChanges.FromSingleColoredArray(
@@ -58,10 +58,10 @@ namespace PixiEditor.Models.Tools.Tools
 
         public IEnumerable<Coordinates> CreateLine(Coordinates start, Coordinates end, int thickness, CapType startCap, CapType endCap)
         {
-            return CreateLine(new[] { end, start }, thickness, startCap, endCap);
+            return CreateLine(new() { end, start }, thickness, startCap, endCap);
         }
 
-        private IEnumerable<Coordinates> CreateLine(Coordinates[] coordinates, int thickness, CapType startCap, CapType endCap)
+        private IEnumerable<Coordinates> CreateLine(List<Coordinates> coordinates, int thickness, CapType startCap, CapType endCap)
         {
             Coordinates startingCoordinates = coordinates[^1];
             Coordinates latestCoordinates = coordinates[0];

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

@@ -107,7 +107,8 @@ namespace PixiEditor.Models.Tools.Tools
             ResetSelectionValues(startPos);
 
             // Move offset if no selection
-            Selection selection = ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection;
+            var doc = ViewModelMain.Current.BitmapManager.ActiveDocument;
+            Selection selection = doc.ActiveSelection;
             if (selection != null && selection.SelectedPoints.Count > 0)
             {
                 currentSelection = selection.SelectedPoints.ToArray();
@@ -119,13 +120,12 @@ namespace PixiEditor.Models.Tools.Tools
 
             if (Keyboard.IsKeyDown(Key.LeftCtrl) || MoveAll)
             {
-                affectedLayers = ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.Where(x => x.IsVisible)
+                affectedLayers = doc.Layers.Where(x => x.IsVisible)
                     .ToArray();
             }
             else
             {
-                affectedLayers = ViewModelMain.Current.BitmapManager.ActiveDocument
-                    .Layers.Where(x => x.IsActive && x.IsVisible).ToArray();
+                affectedLayers = doc.Layers.Where(x => x.IsActive && doc.GetFinalLayerIsVisible(x)).ToArray();
             }
 
             startSelection = currentSelection;
@@ -133,7 +133,7 @@ namespace PixiEditor.Models.Tools.Tools
             startingOffsets = GetOffsets(affectedLayers);
         }
 
-        public override LayerChange[] Use(Layer layer, Coordinates[] mouseMove, Color color)
+        public override LayerChange[] Use(Layer layer, List<Coordinates> mouseMove, Color color)
         {
             LayerChange[] result = new LayerChange[affectedLayers.Length];
             var end = mouseMove[0];
@@ -163,9 +163,9 @@ namespace PixiEditor.Models.Tools.Tools
             return result;
         }
 
-        public BitmapPixelChanges MoveSelection(Layer layer, Coordinates[] mouseMove)
+        public BitmapPixelChanges MoveSelection(Layer layer, IEnumerable<Coordinates> mouseMove)
         {
-            Coordinates end = mouseMove[0];
+            Coordinates end = mouseMove.First();
 
             currentSelection = TranslateSelection(end);
             if (updateViewModelSelection)

+ 3 - 2
PixiEditor/Models/Tools/Tools/MoveViewportTool.cs

@@ -1,4 +1,5 @@
-using System.Drawing;
+using System.Collections.Generic;
+using System.Drawing;
 using System.Windows.Input;
 using PixiEditor.Models.Position;
 using PixiEditor.ViewModels;
@@ -44,7 +45,7 @@ namespace PixiEditor.Models.Tools.Tools
             }
         }
 
-        public override void Use(Coordinates[] pixels)
+        public override void Use(List<Coordinates> pixels)
         {
         }
     }

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

@@ -32,6 +32,7 @@ namespace PixiEditor.Models.Tools.Tools
             toolSizeSetting = Toolbar.GetSetting<SizeSetting>("ToolSize");
             pixelPerfectSetting = Toolbar.GetSetting<BoolSetting>("PixelPerfectEnabled");
             pixelPerfectSetting.ValueChanged += PixelPerfectSettingValueChanged;
+            RequiresPreviewLayer = pixelPerfectSetting.Value;
             lineTool = new LineTool();
             ClearPreviewLayerOnEachIteration = false;
         }
@@ -44,9 +45,9 @@ namespace PixiEditor.Models.Tools.Tools
             confirmedPixels.Clear();
         }
 
-        public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
+        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
         {
-            Coordinates startingCords = coordinates.Length > 1 ? coordinates[1] : coordinates[0];
+            Coordinates startingCords = coordinates.Count > 1 ? coordinates[1] : coordinates[0];
             BitmapPixelChanges pixels = Draw(
                 startingCords,
                 coordinates[0],

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

@@ -37,7 +37,7 @@ namespace PixiEditor.Models.Tools.Tools
             }
         }
 
-        public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
+        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
         {
             int thickness = Toolbar.GetSetting<SizeSetting>("ToolSize").Value;
             BitmapPixelChanges pixels =
@@ -54,7 +54,7 @@ namespace PixiEditor.Models.Tools.Tools
             return new[] { new LayerChange(pixels, layer) };
         }
 
-        public IEnumerable<Coordinates> CreateRectangle(Coordinates[] coordinates, int thickness)
+        public IEnumerable<Coordinates> CreateRectangle(List<Coordinates> coordinates, int thickness)
         {
             DoubleCords fixedCoordinates = CalculateCoordinatesForShapeRotation(coordinates[^1], coordinates[0]);
             List<Coordinates> output = new List<Coordinates>();
@@ -80,7 +80,7 @@ namespace PixiEditor.Models.Tools.Tools
 
         public IEnumerable<Coordinates> CreateRectangle(Coordinates start, Coordinates end, int thickness)
         {
-            return CreateRectangle(new[] { end, start }, thickness);
+            return CreateRectangle(new() { end, start }, thickness);
         }
 
         public IEnumerable<Coordinates> CalculateFillForRectangle(Coordinates start, Coordinates end, int thickness)

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

@@ -52,7 +52,7 @@ namespace PixiEditor.Models.Tools.Tools
             SelectionHelpers.AddSelectionUndoStep(ViewModelMain.Current.BitmapManager.ActiveDocument, oldSelectedPoints, SelectionType);
         }
 
-        public override void Use(Coordinates[] pixels)
+        public override void Use(List<Coordinates> pixels)
         {
             Select(pixels, Toolbar.GetEnumSetting<SelectionShape>("SelectShape").Value);
         }
@@ -90,7 +90,7 @@ namespace PixiEditor.Models.Tools.Tools
             return GetRectangleSelectionForPoints(new Coordinates(0, 0), new Coordinates(document.Width - 1, document.Height - 1));
         }
 
-        private void Select(Coordinates[] pixels, SelectionShape shape)
+        private void Select(List<Coordinates> pixels, SelectionShape shape)
         {
             IEnumerable<Coordinates> selection;
 

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

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Windows;
 using System.Windows.Input;
 using PixiEditor.Models.Position;
@@ -79,7 +80,7 @@ namespace PixiEditor.Models.Tools.Tools
             ViewModelMain.Current.BitmapManager.ActiveDocument.ZoomPercentage = percentage;
         }
 
-        public override void Use(Coordinates[] pixels)
+        public override void Use(List<Coordinates> pixels)
         {
         }
     }

+ 6 - 8
PixiEditor/Models/Undo/StorageBasedChange.cs

@@ -1,13 +1,11 @@
-using System;
-using System.Buffers.Text;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.Layers;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Text;
-using Newtonsoft.Json;
-using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.IO;
-using PixiEditor.Models.Layers;
 
 namespace PixiEditor.Models.Undo
 {
@@ -90,7 +88,7 @@ namespace PixiEditor.Models.Undo
                     IsActive = storedLayer.IsActive,
                     Width = storedLayer.Width,
                     Height = storedLayer.Height,
-                    LayerHighlightColor = storedLayer.LayerHighlightColor                    
+                    LayerHighlightColor = storedLayer.LayerHighlightColor
                 };
                 layers[i].ChangeGuid(storedLayer.LayerGuid);
 
@@ -232,4 +230,4 @@ namespace PixiEditor.Models.Undo
             }
         }
     }
-}
+}

+ 18 - 3
PixiEditor/PixiEditor.csproj

@@ -114,8 +114,13 @@
 
   <ItemGroup>
     <None Remove="Images\AnchorDot.png" />
+    <None Remove="Images\CheckerTile.png" />
+    <None Remove="Images\DiagonalRed.png" />
     <None Remove="Images\Eye-off.png" />
     <None Remove="Images\Eye.png" />
+    <None Remove="Images\Folder-add.png" />
+    <None Remove="Images\Folder.png" />
+    <None Remove="Images\Layer-add.png" />
     <None Remove="Images\JpgFile.png" />
     <None Remove="Images\MoveImage.png" />
     <None Remove="Images\MoveViewportImage.png" />
@@ -125,6 +130,7 @@
     <None Remove="Images\PixiParserLogo.png" />
     <None Remove="Images\PngFile.png" />
     <None Remove="Images\SelectImage.png" />
+    <None Remove="Images\Trash.png" />
     <None Remove="Images\UnknownFile.png" />
     <None Remove="Images\ZoomImage.png" />
     <None Include="..\icon.ico">
@@ -137,29 +143,37 @@
     </None>
   </ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Dirkster.AvalonDock" Version="4.50.3" />
+    <PackageReference Include="Dirkster.AvalonDock" Version="4.51.1" />
     <PackageReference Include="DiscordRichPresence" Version="1.0.175" />
     <PackageReference Include="Expression.Blend.Sdk">
       <Version>1.0.2</Version>
+      <NoWarn>NU1701</NoWarn>
     </PackageReference>
     <PackageReference Include="Extended.Wpf.Toolkit" Version="3.8.2" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
-    <PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
+    <PackageReference Include="MvvmLightLibs" Version="5.4.1.1">
+      <NoWarn>NU1701</NoWarn>
+    </PackageReference>
     <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
     <PackageReference Include="PixiEditor.ColorPicker" Version="3.1.0" />
-    <PackageReference Include="PixiEditor.Parser" Version="1.1.2.1" />
+    <PackageReference Include="PixiEditor.Parser" Version="1.1.3.1" />
     <PackageReference Include="WriteableBitmapEx">
       <Version>1.6.7</Version>
     </PackageReference>
   </ItemGroup>
   <ItemGroup>
     <Resource Include="Images\AnchorDot.png" />
+    <Resource Include="Images\CheckerTile.png" />
+    <Resource Include="Images\DiagonalRed.png" />
     <Resource Include="Images\FloodFillImage.png" />
     <Resource Include="Images\CircleImage.png" />
     <Resource Include="Images\EraserImage.png" />
     <Resource Include="Images\BrightnessImage.png" />
     <Resource Include="Images\Eye-off.png" />
     <Resource Include="Images\Eye.png" />
+    <Resource Include="Images\Folder-add.png" />
+    <Resource Include="Images\Folder.png" />
+    <Resource Include="Images\Layer-add.png" />
     <Resource Include="Images\JpgFile.png" />
     <Resource Include="Images\LineImage.png" />
     <Resource Include="Images\MoveImage.png" />
@@ -174,6 +188,7 @@
     <Resource Include="Images\RectangleImage.png" />
     <Resource Include="Images\SelectImage.png" />
     <Resource Include="Images\transparentbg.png" />
+    <Resource Include="Images\Trash.png" />
     <Resource Include="Images\UnknownFile.png" />
     <Resource Include="Images\ZoomImage.png" />
   </ItemGroup>

+ 28 - 29
PixiEditor/Styles/DarkScrollBarStyle.xaml

@@ -13,6 +13,28 @@
     <SolidColorBrush x:Key="HorizontalNormalBrush" Color="#FF686868" />
     <SolidColorBrush x:Key="HorizontalNormalBorderBrush" Color="#888" />
 
+    <Style TargetType="{x:Type ScrollViewer}">
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="{x:Type ScrollViewer}">
+                    <Grid>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition/>
+                            <ColumnDefinition Width="Auto"/>
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition/>
+                            <RowDefinition Height="Auto"/>
+                        </Grid.RowDefinitions>
+                        <ScrollContentPresenter Grid.Column="0" />
+                        <ScrollBar x:Name="PART_VerticalScrollBar" Grid.Row="0" Grid.Column="1" Value="{TemplateBinding VerticalOffset}" Maximum="{TemplateBinding ScrollableHeight}" ViewportSize="{TemplateBinding ViewportHeight}" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
+                        <ScrollBar x:Name="PART_HorizontalScrollBar" Orientation="Horizontal" Grid.Row="1" Grid.Column="0" Value="{TemplateBinding HorizontalOffset}" Maximum="{TemplateBinding ScrollableWidth}" ViewportSize="{TemplateBinding ViewportWidth}" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
+                    </Grid>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+
     <LinearGradientBrush x:Key="ListBoxBackgroundBrush" StartPoint="0,0" EndPoint="1,0.001">
         <GradientBrush.GradientStops>
             <GradientStopCollection>
@@ -87,7 +109,7 @@
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate TargetType="{x:Type Thumb}">
-                    <Border CornerRadius="4" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="0"  Width="8" Margin="8,0,-2,0"/>
+                    <Border CornerRadius="4" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="0"  Width="Auto" Margin="8,0,-2,0"/>
                 </ControlTemplate>
             </Setter.Value>
         </Setter>
@@ -119,18 +141,18 @@
     <ControlTemplate x:Key="HorizontalScrollBar" TargetType="{x:Type ScrollBar}">
         <Grid>
             <Grid.ColumnDefinitions>
-                <ColumnDefinition MaxWidth="18"/>
-                <ColumnDefinition Width="0.00001*"/>
-                <ColumnDefinition MaxWidth="18"/>
+                <ColumnDefinition MaxWidth="2"/>
+                <ColumnDefinition Width="1*"/>
+                <ColumnDefinition MaxWidth="2"/>
             </Grid.ColumnDefinitions>
-            <Border Grid.ColumnSpan="3" CornerRadius="2" Background="#F0F0F0"/>
+            <Border Grid.ColumnSpan="3" CornerRadius="2" Background="Transparent"/>
             <RepeatButton Grid.Column="0"  Style="{StaticResource ScrollBarLineButton}" Width="18" Command="ScrollBar.LineLeftCommand" Content="M 4 0 L 4 8 L 0 4 Z" />
             <Track Name="PART_Track" Grid.Column="1" IsDirectionReversed="False">
                 <Track.DecreaseRepeatButton>
                     <RepeatButton Style="{StaticResource ScrollBarPageButton}" Command="ScrollBar.PageLeftCommand" />
                 </Track.DecreaseRepeatButton>
                 <Track.Thumb>
-                    <Thumb Style="{StaticResource ScrollBarThumb}" Margin="0,1,0,1" Background="{StaticResource NormalBrush}" BorderBrush="{StaticResource NormalBorderBrush}" />
+                    <Thumb Height="10" Style="{StaticResource ScrollBarThumb}" Margin="0,1,0,1" Background="{StaticResource NormalBrush}" BorderBrush="{StaticResource NormalBorderBrush}" />
                 </Track.Thumb>
                 <Track.IncreaseRepeatButton>
                     <RepeatButton Style="{StaticResource ScrollBarPageButton}" Command="ScrollBar.PageRightCommand" />
@@ -156,27 +178,4 @@
         </Style.Triggers>
     </Style>
 
-    <Style x:Key="FavsScrollViewer" TargetType="{x:Type ScrollViewer}">
-        <Setter Property="OverridesDefaultStyle" Value="True"/>
-        <Setter Property="Template">
-            <Setter.Value>
-                <ControlTemplate TargetType="{x:Type ScrollViewer}">
-                    <Grid>
-                        <Grid.ColumnDefinitions>
-                            <ColumnDefinition Width="Auto"/>
-                            <ColumnDefinition/>
-                        </Grid.ColumnDefinitions>
-                        <Grid.RowDefinitions>
-                            <RowDefinition/>
-                            <RowDefinition Height="Auto"/>
-                        </Grid.RowDefinitions>
-                        <ScrollContentPresenter Grid.Column="1"/>
-                        <ScrollBar Name="PART_VerticalScrollBar" Value="{TemplateBinding VerticalOffset}" Maximum="{TemplateBinding ScrollableHeight}" ViewportSize="{TemplateBinding ViewportHeight}" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
-                        <ScrollBar Name="PART_HorizontalScrollBar" Orientation="Horizontal" Grid.Row="1" Grid.Column="1" Value="{TemplateBinding HorizontalOffset}" Maximum="{TemplateBinding ScrollableWidth}" ViewportSize="{TemplateBinding ViewportWidth}" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
-                    </Grid>
-                </ControlTemplate>
-            </Setter.Value>
-        </Setter>
-    </Style>
-
 </ResourceDictionary>

+ 28 - 0
PixiEditor/Styles/ListSwitchButtonStyle.xaml

@@ -0,0 +1,28 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:local="clr-namespace:PixiEditor.Views.UserControls">
+
+    <Style TargetType="{x:Type local:ListSwitchButton}" BasedOn="{StaticResource ResourceKey={x:Type Button}}" x:Name="btn">
+        <Setter Property="BorderBrush" Value="Black"/>
+        <Setter Property="BorderThickness" Value="1"/>
+        <Setter Property="FontSize" Value="12"/>
+        <Setter Property="Cursor" Value="Hand"/>
+        <Setter Property="Padding" Value="2, 0"/>
+        <Setter Property="Foreground" Value="White"/>
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="{x:Type local:ListSwitchButton}">
+                    <Border
+                            CornerRadius="1"
+                        BorderBrush="{TemplateBinding BorderBrush}"
+                            Background="{Binding Path=ActiveItem.Background, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:ListSwitchButton}}}"
+                        BorderThickness="{TemplateBinding BorderThickness}">
+                        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Path=ActiveItem.Content, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:ListSwitchButton}}}"
+                                   FontSize="{TemplateBinding FontSize}" Padding="{TemplateBinding Padding}"/>
+                    </Border>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+
+</ResourceDictionary>

+ 129 - 0
PixiEditor/Styles/TreeViewStyle.xaml

@@ -0,0 +1,129 @@
+<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+                    xmlns:views="clr-namespace:PixiEditor.Views.UserControls"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" xmlns:local="clr-namespace:PixiEditor.Helpers.UI">
+
+    <converters:IndentConverter x:Key="IndentConverter" />
+    
+    <Style TargetType="TreeView">
+        <Setter Property="Background" Value="{StaticResource AccentColor}"/>
+        <Setter Property="BorderThickness" Value="0"/>
+    </Style>
+
+    <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#3A3A3D"/>
+    <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}"
+                      Color="Black" />
+    <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}"
+                      Color="Transparent" />
+    <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}"
+                      Color="Black" />
+    <PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
+
+    <Style x:Key="TreeViewItemFocusVisual">
+        <Setter Property="Control.Template">
+            <Setter.Value>
+                <ControlTemplate>
+                    <Rectangle/>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+
+    <Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}" >
+        <Setter Property="Focusable" Value="False"/>
+        <Setter Property="Width" Value="16"/>
+        <Setter Property="Height" Value="16"/>
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="{x:Type ToggleButton}">
+                    <Border Width="20" Height="20" Background="Transparent" Padding="5,5,5,5">
+                        <Path x:Name="ExpandPath" Fill="Transparent" Stroke="White" Data="{StaticResource TreeArrow}">
+                            <Path.RenderTransform>
+                                <RotateTransform Angle="135" CenterX="3" CenterY="3"/>
+                            </Path.RenderTransform>
+                        </Path>
+                    </Border>
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="IsMouseOver" Value="True">
+                            <Setter Property="Stroke" TargetName="ExpandPath" Value="#FFA8A8A8"/>
+                            <Setter Property="Fill" TargetName="ExpandPath" Value="#FFA8A8A8"/>
+                        </Trigger>
+                        <Trigger Property="IsChecked" Value="True">
+                            <Setter Property="RenderTransform" TargetName="ExpandPath">
+                                <Setter.Value>
+                                    <RotateTransform Angle="180" CenterX="3" CenterY="3"/>
+                                </Setter.Value>
+                            </Setter>
+                            <Setter Property="Fill" TargetName="ExpandPath" Value="White"/>
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+    <Style x:Key="TreeViewItemStyle" TargetType="{x:Type TreeViewItem}">
+        <Setter Property="Background" Value="Transparent"/>
+        <Setter Property="HorizontalContentAlignment" Value="{Binding Path=HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
+        <Setter Property="Padding" Value="0,0,0,0"/>
+        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
+        <Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}"/>
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="{x:Type TreeViewItem}">
+                    <Grid>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition MinWidth="15" Width="Auto"/>
+                            <ColumnDefinition Width="Auto"/>
+                            <ColumnDefinition Width="*"/>
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="Auto"/>
+                            <RowDefinition/>
+                        </Grid.RowDefinitions>
+                        <ToggleButton x:Name="Expander"  Style="{StaticResource ExpandCollapseToggleStyle}" ClickMode="Press" IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" />
+                        <Border x:Name="Bd" SnapsToDevicePixels="true" Grid.Column="1" Grid.ColumnSpan="2" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
+                            <ContentPresenter x:Name="PART_Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" ContentSource="Header"/>
+                        </Border>
+                        <ItemsPresenter x:Name="ItemsHost" Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="1"
+                                        local:TreeViewItemHelper.Indent="{Binding Path=(local:TreeViewItemHelper.Indent), Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}, Converter={StaticResource IndentConverter}}"/>
+                    </Grid>
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="IsExpanded" Value="false">
+                            <Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
+                        </Trigger>
+                        <Trigger Property="HasItems" Value="false">
+                            <Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
+                        </Trigger>
+                        <MultiTrigger>
+                            <MultiTrigger.Conditions>
+                                <Condition Property="HasItems" Value="True"/>
+                                <Condition Property="IsSelected" Value="True"/>
+                            </MultiTrigger.Conditions>
+                            <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
+                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
+                        </MultiTrigger>
+                        <MultiTrigger>
+                            <MultiTrigger.Conditions>
+                                <Condition Property="IsSelected" Value="true"/>
+                                <Condition Property="IsSelectionActive" Value="false"/>
+                            </MultiTrigger.Conditions>
+                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
+                        </MultiTrigger>
+                        <Trigger Property="IsEnabled" Value="false">
+                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+
+    <Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource TreeViewItemStyle}">
+        <Style.Triggers>
+            <Trigger Property="HasItems" Value="True">
+                <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
+            </Trigger>
+        </Style.Triggers>
+        <Setter Property="HorizontalContentAlignment" Value="Stretch" />
+    </Style>
+
+</ResourceDictionary>

+ 0 - 1
PixiEditor/ViewModels/SaveFilePopupViewModel.cs

@@ -69,7 +69,6 @@ namespace PixiEditor.ViewModels
         /// <summary>
         ///     Command that handles Path choosing to save file
         /// </summary>
-        /// <param name="parameter"></param>
         private void ChoosePath(object parameter)
         {
             SaveFileDialog path = new SaveFileDialog

+ 5 - 4
PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs

@@ -55,11 +55,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         private void Copy(object parameter)
         {
+            var doc = Owner.BitmapManager.ActiveDocument;
             ClipboardController.CopyToClipboard(
-                Owner.BitmapManager.ActiveDocument.Layers.Where(x => x.IsActive && x.IsVisible).ToArray(),
-                Owner.BitmapManager.ActiveDocument.ActiveSelection.SelectedPoints.ToArray(),
-                Owner.BitmapManager.ActiveDocument.Width,
-                Owner.BitmapManager.ActiveDocument.Height);
+                doc.Layers.Where(x => x.IsActive && doc.GetFinalLayerIsVisible(x)).ToArray(),
+                doc.ActiveSelection.SelectedPoints.ToArray(),
+                doc.Width,
+                doc.Height);
         }
     }
 }

+ 4 - 2
PixiEditor/ViewModels/SubViewModels/Main/DocumentViewModel.cs

@@ -47,14 +47,16 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
                     return;
                 }
             }
+
             Owner.BitmapManager.CloseDocument(document);
         }
 
         private void DeletePixels(object parameter)
         {
+            var doc = Owner.BitmapManager.ActiveDocument;
             Owner.BitmapManager.BitmapOperations.DeletePixels(
-                Owner.BitmapManager.ActiveDocument.Layers.Where(x => x.IsActive && x.IsVisible).ToArray(),
-                Owner.BitmapManager.ActiveDocument.ActiveSelection.SelectedPoints.ToArray());
+                doc.Layers.Where(x => x.IsActive && doc.GetFinalLayerIsVisible(x)).ToArray(),
+                doc.ActiveSelection.SelectedPoints.ToArray());
         }
 
         private void OpenResizePopup(object parameter)

+ 0 - 2
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -9,10 +9,8 @@ using Microsoft.Win32;
 using Newtonsoft.Json.Linq;
 using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
-using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
-using PixiEditor.Models.Enums;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Parser;

+ 197 - 21
PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -1,9 +1,10 @@
-using System;
-using System.Linq;
-using System.Windows.Input;
-using PixiEditor.Helpers;
+using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Layers;
+using PixiEditor.Views.UserControls;
+using System;
+using System.Linq;
+using System.Windows.Input;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {
@@ -13,12 +14,22 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public RelayCommand NewLayerCommand { get; set; }
 
+        public RelayCommand NewGroupCommand { get; set; }
+
+        public RelayCommand CreateGroupFromActiveLayersCommand { get; set; }
+
+        public RelayCommand DeleteSelectedCommand { get; set; }
+
+        public RelayCommand DeleteGroupCommand { get; set; }
+
         public RelayCommand DeleteLayersCommand { get; set; }
 
         public RelayCommand DuplicateLayerCommand { get; set; }
 
         public RelayCommand RenameLayerCommand { get; set; }
 
+        public RelayCommand RenameGroupCommand { get; set; }
+
         public RelayCommand MoveToBackCommand { get; set; }
 
         public RelayCommand MoveToFrontCommand { get; set; }
@@ -34,6 +45,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             SetActiveLayerCommand = new RelayCommand(SetActiveLayer);
             NewLayerCommand = new RelayCommand(NewLayer, CanCreateNewLayer);
+            NewGroupCommand = new RelayCommand(NewGroup, CanCreateNewLayer);
+            CreateGroupFromActiveLayersCommand = new RelayCommand(CreateGroupFromActiveLayers, CanCreateGroupFromSelected);
             DeleteLayersCommand = new RelayCommand(DeleteLayer, CanDeleteLayer);
             DuplicateLayerCommand = new RelayCommand(DuplicateLayer, CanDuplicateLayer);
             MoveToBackCommand = new RelayCommand(MoveLayerToBack, CanMoveToBack);
@@ -42,17 +55,134 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             MergeSelectedCommand = new RelayCommand(MergeSelected, CanMergeSelected);
             MergeWithAboveCommand = new RelayCommand(MergeWithAbove, CanMergeWithAbove);
             MergeWithBelowCommand = new RelayCommand(MergeWithBelow, CanMergeWithBelow);
+            RenameGroupCommand = new RelayCommand(RenameGroup);
+            DeleteGroupCommand = new RelayCommand(DeleteGroup);
+            DeleteSelectedCommand = new RelayCommand(DeleteSelected, CanDeleteSelected);
             Owner.BitmapManager.DocumentChanged += BitmapManager_DocumentChanged;
         }
 
+        public void CreateGroupFromActiveLayers(object parameter)
+        {
+            // var doc = Owner.BitmapManager.ActiveDocument;
+            // if (doc != null)
+            // {
+            //    doc.LayerStructure.AddNewGroup($"{Owner.BitmapManager.ActiveLayer.Name} Group", doc.Layers.Where(x => x.IsActive).Reverse(), Owner.BitmapManager.ActiveDocument.ActiveLayerGuid);
+            // }
+        }
+
+        public bool CanDeleteSelected(object parameter)
+        {
+            return (parameter is not null and (Layer or LayerGroup)) || (Owner.BitmapManager?.ActiveDocument?.ActiveLayer != null);
+        }
+
+        public void DeleteSelected(object parameter)
+        {
+            if (parameter is Layer layer)
+            {
+                DeleteLayer(Owner.BitmapManager.ActiveDocument.Layers.IndexOf(layer));
+            }
+            else if (parameter is LayerStructureItemContainer container)
+            {
+                DeleteLayer(Owner.BitmapManager.ActiveDocument.Layers.IndexOf(container.Layer));
+            }
+            else if (parameter is LayerGroup group)
+            {
+                DeleteGroup(group.GroupGuid);
+            }
+            else if (parameter is LayerGroupControl groupControl)
+            {
+                DeleteGroup(groupControl.GroupGuid);
+            }
+            else if (Owner.BitmapManager.ActiveDocument.ActiveLayer != null)
+            {
+                DeleteLayer(Owner.BitmapManager.ActiveDocument.Layers.IndexOf(Owner.BitmapManager.ActiveDocument.ActiveLayer));
+            }
+        }
+
+        public void DeleteGroup(object parameter)
+        {
+            if (parameter is Guid guid)
+            {
+                var group = Owner.BitmapManager.ActiveDocument?.LayerStructure.GetGroupByGuid(guid);
+                var layers = Owner.BitmapManager.ActiveDocument?.LayerStructure.GetGroupLayers(group);
+                foreach (var layer in layers)
+                {
+                    layer.IsActive = true;
+                }
+
+                Owner.BitmapManager.ActiveDocument?.RemoveActiveLayers();
+            }
+        }
+
+        public void RenameGroup(object parameter)
+        {
+            if (parameter is Guid guid)
+            {
+                var group = Owner.BitmapManager.ActiveDocument?.LayerStructure.GetGroupByGuid(guid);
+                group.IsRenaming = true;
+            }
+        }
+
+        public void NewGroup(object parameter)
+        {
+            GuidStructureItem control = GetGroupFromParameter(parameter);
+            var doc = Owner.BitmapManager.ActiveDocument;
+            if (doc != null)
+            {
+                var lastGroups = doc.LayerStructure.CloneGroups();
+                if (parameter is Layer or LayerStructureItemContainer)
+                {
+                    GuidStructureItem group = doc.LayerStructure.AddNewGroup($"{doc.ActiveLayer.Name} Group", doc.ActiveLayer.LayerGuid);
+
+                    Owner.BitmapManager.ActiveDocument.LayerStructure.ExpandParentGroups(group);
+                }
+                else if (control != null)
+                {
+                    doc.LayerStructure.AddNewGroup($"{control.Name} Group", control);
+                }
+
+                doc.AddLayerStructureToUndo(lastGroups);
+                doc.RaisePropertyChange(nameof(doc.LayerStructure));
+            }
+        }
+
+        public bool CanAddNewGroup(object property)
+        {
+            return CanCreateNewLayer(property) && Owner.BitmapManager.ActiveLayer != null;
+        }
+
         public bool CanMergeSelected(object obj)
         {
             return Owner.BitmapManager.ActiveDocument?.Layers.Count(x => x.IsActive) > 1;
         }
 
+        public bool CanCreateGroupFromSelected(object obj)
+        {
+            return Owner.BitmapManager.ActiveDocument?.Layers.Count(x => x.IsActive) > 0;
+        }
+
         public void NewLayer(object parameter)
         {
-            Owner.BitmapManager.ActiveDocument.AddNewLayer($"New Layer");
+            GuidStructureItem control = GetGroupFromParameter(parameter);
+            var doc = Owner.BitmapManager.ActiveDocument;
+            var activeLayerParent = doc.LayerStructure.GetGroupByLayer(doc.ActiveLayerGuid);
+
+            Guid lastActiveLayerGuid = doc.ActiveLayerGuid;
+
+            doc.AddNewLayer($"New Layer {Owner.BitmapManager.ActiveDocument.Layers.Count}");
+
+            if (doc.Layers.Count > 1)
+            {
+                doc.MoveLayerInStructure(doc.Layers[^1].LayerGuid, lastActiveLayerGuid, true);
+                Guid? parent = parameter is Layer or LayerStructureItemContainer ? activeLayerParent?.GroupGuid : activeLayerParent.Parent?.GroupGuid;
+                doc.LayerStructure.AssignParent(doc.ActiveLayerGuid, parent);
+                doc.UndoManager.UndoStack.Pop();
+            }
+            if (control != null)
+            {
+                control.IsExpanded = true;
+                doc.RaisePropertyChange(nameof(doc.LayerStructure));
+            }
         }
 
         public bool CanCreateNewLayer(object parameter)
@@ -64,36 +194,31 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             int index = (int)parameter;
 
-            if (Owner.BitmapManager.ActiveDocument.Layers[index].IsActive && Mouse.RightButton == MouseButtonState.Pressed)
+            var doc = Owner.BitmapManager.ActiveDocument;
+
+            if (doc.Layers[index].IsActive && Mouse.RightButton == MouseButtonState.Pressed)
             {
                 return;
             }
 
             if (Keyboard.IsKeyDown(Key.LeftCtrl))
             {
-                Owner.BitmapManager.ActiveDocument.ToggleLayer(index);
+                doc.ToggleLayer(index);
             }
             else if (Keyboard.IsKeyDown(Key.LeftShift) && Owner.BitmapManager.ActiveDocument.Layers.Any(x => x.IsActive))
             {
-                Owner.BitmapManager.ActiveDocument.SelectLayersRange(index);
+                doc.SelectLayersRange(index);
             }
             else
             {
-                Owner.BitmapManager.ActiveDocument.SetMainActiveLayer(index);
+                doc.SetMainActiveLayer(index);
             }
         }
 
         public void DeleteLayer(object parameter)
         {
-            int index = (int)parameter;
-            if (!Owner.BitmapManager.ActiveDocument.Layers[index].IsActive)
-            {
-                Owner.BitmapManager.ActiveDocument.RemoveLayer(index);
-            }
-            else
-            {
-                Owner.BitmapManager.ActiveDocument.RemoveActiveLayers();
-            }
+            var doc = Owner.BitmapManager.ActiveDocument;
+            doc.RemoveActiveLayers();
         }
 
         public bool CanDeleteLayer(object property)
@@ -131,22 +256,36 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public void MoveLayerToFront(object parameter)
         {
             int oldIndex = (int)parameter;
-            Owner.BitmapManager.ActiveDocument.MoveLayerIndexBy(oldIndex, 1);
+            Guid layerToMove = Owner.BitmapManager.ActiveDocument.Layers[oldIndex].LayerGuid;
+            Guid referenceLayer = Owner.BitmapManager.ActiveDocument.Layers[oldIndex + 1].LayerGuid;
+            Owner.BitmapManager.ActiveDocument.MoveLayerInStructure(layerToMove, referenceLayer, true);
         }
 
         public void MoveLayerToBack(object parameter)
         {
             int oldIndex = (int)parameter;
-            Owner.BitmapManager.ActiveDocument.MoveLayerIndexBy(oldIndex, -1);
+            Guid layerToMove = Owner.BitmapManager.ActiveDocument.Layers[oldIndex].LayerGuid;
+            Guid referenceLayer = Owner.BitmapManager.ActiveDocument.Layers[oldIndex - 1].LayerGuid;
+            Owner.BitmapManager.ActiveDocument.MoveLayerInStructure(layerToMove, referenceLayer, false);
         }
 
         public bool CanMoveToFront(object property)
         {
+            if (property == null)
+            {
+                return false;
+            }
+
             return Owner.DocumentIsNotNull(null) && Owner.BitmapManager.ActiveDocument.Layers.Count - 1 > (int)property;
         }
 
         public bool CanMoveToBack(object property)
         {
+            if (property == null)
+            {
+                return false;
+            }
+
             return (int)property > 0;
         }
 
@@ -173,6 +312,10 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public bool CanMergeWithAbove(object property)
         {
+            if (property == null)
+            {
+                return false;
+            }
             int index = (int)property;
             return Owner.DocumentIsNotNull(null) && index != Owner.BitmapManager.ActiveDocument.Layers.Count - 1
                 && Owner.BitmapManager.ActiveDocument.Layers.Count(x => x.IsActive) == 1;
@@ -180,10 +323,38 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public bool CanMergeWithBelow(object property)
         {
+            if (property == null)
+            {
+                return false;
+            }
+
             int index = (int)property;
             return Owner.DocumentIsNotNull(null) && index != 0 && Owner.BitmapManager.ActiveDocument.Layers.Count(x => x.IsActive) == 1;
         }
 
+        private GuidStructureItem GetGroupFromParameter(object parameter)
+        {
+            if (parameter is LayerGroupControl)
+            {
+                return ((LayerGroupControl)parameter).GroupData;
+            }
+            else if (parameter is Layer || parameter is LayerStructureItemContainer)
+            {
+                Guid layerGuid = parameter is Layer layer ? layer.LayerGuid : ((LayerStructureItemContainer)parameter).Layer.LayerGuid;
+                var group = Owner.BitmapManager.ActiveDocument.LayerStructure.GetGroupByLayer(layerGuid);
+                if (group != null)
+                {
+                    while (group.IsExpanded && group.Parent != null)
+                    {
+                        group = group.Parent;
+                    }
+                }
+                return group;
+            }
+
+            return null;
+        }
+
         private void BitmapManager_DocumentChanged(object sender, Models.Events.DocumentChangedEventArgs e)
         {
             if (e.OldDocument != null)
@@ -202,7 +373,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             if (e.LayerChangeType == Models.Enums.LayerAction.SetActive)
             {
                 Owner.BitmapManager.ActiveDocument.UpdateLayersColor();
+                Owner.BitmapManager.ActiveDocument.LayerStructure.ExpandParentGroups(e.LayerAffectedGuid);
+            }
+            else
+            {
+                Owner.BitmapManager.ActiveDocument.ChangesSaved = false;
             }
         }
     }
-}
+}

+ 22 - 17
PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs

@@ -84,30 +84,35 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         private static void AskToInstall()
         {
-            string dir = AppDomain.CurrentDomain.BaseDirectory;
-            UpdateDownloader.CreateTempDirectory();
-            bool updateZipExists = Directory.GetFiles(UpdateDownloader.DownloadLocation, "update-*.zip").Length > 0;
-            string[] updateExeFiles = Directory.GetFiles(UpdateDownloader.DownloadLocation, "update-*.exe");
-            bool updateExeExists = updateExeFiles.Length > 0;
+#if RELEASE
+            if (IPreferences.Current.GetPreference("CheckUpdatesOnStartup", true))
+            {
+                string dir = AppDomain.CurrentDomain.BaseDirectory;
+                UpdateDownloader.CreateTempDirectory();
+                bool updateZipExists = Directory.GetFiles(UpdateDownloader.DownloadLocation, "update-*.zip").Length > 0;
+                string[] updateExeFiles = Directory.GetFiles(UpdateDownloader.DownloadLocation, "update-*.exe");
+                bool updateExeExists = updateExeFiles.Length > 0;
 
-            string updaterPath = Path.Join(dir, "PixiEditor.UpdateInstaller.exe");
+                string updaterPath = Path.Join(dir, "PixiEditor.UpdateInstaller.exe");
 
-            if (updateZipExists || updateExeExists)
-            {
-                ViewModelMain.Current.UpdateSubViewModel.UpdateReadyToInstall = true;
-                var result = ConfirmationDialog.Show("Update is ready to install. Do you want to install it now?");
-                if (result == Models.Enums.ConfirmationType.Yes)
+                if (updateZipExists || updateExeExists)
                 {
-                    if (updateZipExists && File.Exists(updaterPath))
-                    {
-                        InstallHeadless(updaterPath);
-                    }
-                    else if (updateExeExists)
+                    ViewModelMain.Current.UpdateSubViewModel.UpdateReadyToInstall = true;
+                    var result = ConfirmationDialog.Show("Update is ready to install. Do you want to install it now?");
+                    if (result == Models.Enums.ConfirmationType.Yes)
                     {
-                        OpenExeInstaller(updateExeFiles[0]);
+                        if (updateZipExists && File.Exists(updaterPath))
+                        {
+                            InstallHeadless(updaterPath);
+                        }
+                        else if (updateExeExists)
+                        {
+                            OpenExeInstaller(updateExeFiles[0]);
+                        }
                     }
                 }
             }
+#endif
         }
 
         private static void InstallHeadless(string updaterPath)

+ 10 - 11
PixiEditor/ViewModels/ViewModelMain.cs

@@ -235,6 +235,15 @@ namespace PixiEditor.ViewModels
         {
             return BitmapManager.ActiveDocument != null;
         }
+        public void CloseWindow(object property)
+        {
+            if (!(property is CancelEventArgs))
+            {
+                throw new ArgumentException();
+            }
+
+            ((CancelEventArgs)property).Cancel = !RemoveDocumentsWithSaveConfirmation();
+        }
 
         [Conditional("DEBUG")]
         private void AddDebugOnlyViewModels()
@@ -259,16 +268,6 @@ namespace PixiEditor.ViewModels
             return new Shortcut(key, ToolsSubViewModel.SelectToolCommand, description, typeof(T), modifier);
         }
 
-        public void CloseWindow(object property)
-        {
-            if (!(property is CancelEventArgs))
-            {
-                throw new ArgumentException();
-            }
-
-            ((CancelEventArgs)property).Cancel = !RemoveDocumentsWithSaveConfirmation();
-        }
-
         /// <summary>
         /// Removes documents with unsaved changes confirmation dialog.
         /// </summary>
@@ -355,4 +354,4 @@ namespace PixiEditor.ViewModels
             }
         }
     }
-}
+}

+ 1 - 2
PixiEditor/Views/Dialogs/HelloTherePopup.xaml.cs

@@ -1,7 +1,6 @@
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.ViewModels.SubViewModels.Main;
-using System.Reflection;
 using System.Windows;
 using System.Windows.Input;
 
@@ -20,7 +19,7 @@ namespace PixiEditor.Views.Dialogs
         public static readonly DependencyProperty RecentlyOpenedEmptyProperty =
             DependencyProperty.Register(nameof(RecentlyOpenedEmpty), typeof(bool), typeof(HelloTherePopup));
 
-        public static string VersionText => 
+        public static string VersionText =>
             $"v{AssemblyHelper.GetCurrentAssemblyVersion(x => $"{x.Major}.{x.Minor}" + (x.Build != 0 ? $".{x.Build}" : ""))}";
 
         public FileViewModel FileViewModel { get => (FileViewModel)GetValue(FileViewModelProperty); set => SetValue(FileViewModelProperty, value); }

+ 0 - 1
PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml.cs

@@ -21,7 +21,6 @@ namespace PixiEditor.Views
             DataContext = this;
         }
 
-
         public int NewHeight
         {
             get => (int)GetValue(NewHeightProperty);

+ 34 - 103
PixiEditor/Views/MainWindow.xaml

@@ -5,29 +5,27 @@
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:local="clr-namespace:PixiEditor"
         xmlns:vm="clr-namespace:PixiEditor.ViewModels"
-        xmlns:vws="clr-namespace:PixiEditor.Views"
+        xmlns:dataHolders="clr-namespace:PixiEditor.Models.DataHolders"
         xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
         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:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker" xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls" xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours" 
-        xmlns:avalonDockTheme="clr-namespace:PixiEditor.Styles.AvalonDock" d:DataContext="{d:DesignInstance Type=vm:ViewModelMain}" xmlns:dataHolders="clr-namespace:PixiEditor.Models.DataHolders"
+        xmlns:avalonDockTheme="clr-namespace:PixiEditor.Styles.AvalonDock" d:DataContext="{d:DesignInstance Type=vm:ViewModelMain}"
         mc:Ignorable="d" WindowStyle="None" Initialized="MainWindow_Initialized"
         Title="PixiEditor" Name="mainWindow" Height="1000" Width="1600" Background="{StaticResource MainColor}"
-        WindowStartupLocation="CenterScreen" WindowState="Maximized"
-        AllowDrop="True" Drop="MainWindow_Drop">
+        WindowStartupLocation="CenterScreen" WindowState="Maximized">
     <WindowChrome.WindowChrome>
         <WindowChrome CaptionHeight="35" x:Name="windowsChrome"/>
     </WindowChrome.WindowChrome>
 
     <Window.Resources>
         <ResourceDictionary>
-            <!--<vm:ViewModelMain x:Key="ViewModelMain" />-->
             <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
             <converters:BoolToIntConverter x:Key="BoolToIntConverter" />
             <converters:NotNullToBoolConverter x:Key="NotNullToBoolConverter" />
-            <converters:FloatNormalizeConverter x:Key="FloatNormalizeConverter" />
+            <converters:LayersToStructuredLayersConverter x:Key="LayersToStructuredLayersConverter"/>
             <converters:DoubleToIntConverter x:Key="DoubleToIntConverter"/>
             <ResourceDictionary.MergedDictionaries>
                 <ResourceDictionary Source="pack://application:,,,/ColorPicker;component/Styles/DefaultColorPickerStyle.xaml" />
@@ -206,7 +204,7 @@
             </ItemsControl>
         </StackPanel>
         <Grid Grid.Column="1" Grid.Row="2" Background="#303030">
-            <Grid>
+            <Grid AllowDrop="True" Drop="MainWindow_Drop">
                 <DockingManager ActiveContent="{Binding BitmapManager.ActiveDocument, Mode=TwoWay}" 
                                            DocumentsSource="{Binding BitmapManager.Documents}">
                     <DockingManager.Theme>
@@ -262,6 +260,7 @@
                         <LayoutPanel Orientation="Horizontal">
                             <LayoutDocumentPane/>
                             <LayoutAnchorablePaneGroup Orientation="Vertical" DockWidth="290">
+
                                 <LayoutAnchorablePane x:Name="colorPane">
                                     <LayoutAnchorable ContentId="colorPicker" Title="Color Picker" CanHide="False"
                                                              CanClose="False" CanAutoHide="False" x:Name="colorPickerPanel"
@@ -296,105 +295,30 @@
                                     <LayoutAnchorable ContentId="layers" Title="Layers" CanHide="False"
                                                          CanClose="False" CanAutoHide="False"
                                                          CanDockAsTabbedDocument="True" CanFloat="True">
-                                        <Grid>
-                                            <Grid.RowDefinitions>
-                                                <RowDefinition Height="40"/>
-                                                <RowDefinition Height="30"/>
-                                                <RowDefinition Height="15"/>
-                                                <RowDefinition Height="1*"/>
-                                            </Grid.RowDefinitions>
-                                            <Button Grid.Row="0" Command="{Binding LayersSubViewModel.NewLayerCommand}" Height="30" Content="New Layer"
-                                            HorizontalAlignment="Stretch" Margin="5"
-                                            Style="{StaticResource DarkRoundButton}" />
-                                            <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="10,0">
-                                                <Label Content="Opacity" Foreground="White" VerticalAlignment="Center"/>
-                                                <vws:NumberInput 
-                                                    Min="0" Max="100"
-                                                    IsEnabled="{Binding Path=BitmapManager.ActiveDocument, 
-                                                    Converter={StaticResource NotNullToBoolConverter}}" 
-                                                    Width="40" Height="20"
-                                                    VerticalAlignment="Center"
-                                                   Value="{Binding BitmapManager.ActiveDocument.ActiveLayer.OpacityUndoTriggerable, Mode=TwoWay, 
-                                            Converter={StaticResource FloatNormalizeConverter}}" />
-                                                <Label Content="%" Foreground="White" VerticalAlignment="Center"/>
-                                            </StackPanel>
-                                            <Separator Grid.Row="2" Background="{StaticResource BrighterAccentColor}"/>
-                                            <ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto">
-                                                <ItemsControl ItemsSource="{Binding BitmapManager.ActiveDocument.Layers}"
-                                                      x:Name="layersItemsControl" AlternationCount="9999">
-                                                    <ItemsControl.ItemsPanel>
-                                                        <ItemsPanelTemplate>
-                                                            <ui:ReversedOrderStackPanel Orientation="Vertical" />
-                                                        </ItemsPanelTemplate>
-                                                    </ItemsControl.ItemsPanel>
-                                                    <ItemsControl.ItemTemplate>
-                                                        <DataTemplate>
-                                                            <vws:LayerItem Tag="{Binding DataContext, ElementName=mainWindow}" LayerIndex="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
-                                Path=(ItemsControl.AlternationIndex)}" SetActiveLayerCommand="{Binding Path=DataContext.LayersSubViewModel.SetActiveLayerCommand, ElementName=mainWindow}"
-                                                                   LayerName="{Binding Name, Mode=TwoWay}" IsActive="{Binding IsActive, Mode=TwoWay}"
-                                                                   IsRenaming="{Binding IsRenaming, Mode=TwoWay}"
-                                                                   PreviewImage="{Binding LayerBitmap}" 
-                                                                   LayerColor="{Binding LayerHighlightColor}"
-                                                                   MoveToBackCommand="{Binding DataContext.LayersSubViewModel.MoveToBackCommand, ElementName=mainWindow}"
-                                                                   MoveToFrontCommand="{Binding DataContext.LayersSubViewModel.MoveToFrontCommand, ElementName=mainWindow}">
-                                                                <vws:LayerItem.ContextMenu>
-                                                                    <ContextMenu>
-                                                                        <MenuItem Header="Delete"
-                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.DeleteLayersCommand,
-                                                                                    RelativeSource={RelativeSource AncestorType=ContextMenu}}"
-
-                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
-                                Path=(ItemsControl.AlternationIndex)}" />
-                                                                        <MenuItem Header="Duplicate"
-                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.DuplicateLayerCommand,
-                                                                                    RelativeSource={RelativeSource AncestorType=ContextMenu}}"
-                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
-                                Path=(ItemsControl.AlternationIndex)}" />
-                                                                        <MenuItem Header="Rename"
-                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.RenameLayerCommand,
-                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
-                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
-                                Path=(ItemsControl.AlternationIndex)}" />
-                                                                        <MenuItem Header="Move to front"
-                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.MoveToFrontCommand, 
-                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
-                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
-                                Path=(ItemsControl.AlternationIndex)}" />
-                                                                        <MenuItem Header="Move to back"
-                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.MoveToBackCommand, 
-                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
-                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
-                                Path=(ItemsControl.AlternationIndex)}" />
-                                                                        <Separator/>
-                                                                        <MenuItem Header="Merge selected"
-                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.MergeSelectedCommand, 
-                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
-                                                                        <MenuItem Header="Merge with above"
-                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.MergeWithAboveCommand, 
-                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
-                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
-                                Path=(ItemsControl.AlternationIndex)}" />
-                                                                        <MenuItem Header="Merge with below"
-                                                                                  Command="{Binding PlacementTarget.Tag.LayersSubViewModel.MergeWithBelowCommand, 
-                                                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
-                                                                                  CommandParameter="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
-                                Path=(ItemsControl.AlternationIndex)}" />
-                                                                    </ContextMenu>
-                                                                </vws:LayerItem.ContextMenu>
-                                                            </vws:LayerItem>
-                                                        </DataTemplate>
-                                                    </ItemsControl.ItemTemplate>
-                                                </ItemsControl>
-                                            </ScrollViewer>
-                                        </Grid>
+                                        <usercontrols:LayersManager                                            
+                                            LayerCommandsViewModel="{Binding LayersSubViewModel}"
+                                            OpacityInputEnabled="{Binding BitmapManager.ActiveDocument, 
+                    Converter={StaticResource NotNullToBoolConverter}}">
+                                            <usercontrols:LayersManager.LayerTreeRoot>
+                                                <MultiBinding Converter="{StaticResource LayersToStructuredLayersConverter}">
+                                                    <Binding Path="BitmapManager.ActiveDocument.Layers" />
+                                                    <Binding Path="BitmapManager.ActiveDocument.LayerStructure"/>
+                                                </MultiBinding>
+                                            </usercontrols:LayersManager.LayerTreeRoot>
+                                        </usercontrols:LayersManager>
+                                    </LayoutAnchorable>
+                                    <LayoutAnchorable x:Name="rawLayerAnchorable" ContentId="rawLayer" Title="Raw layers">
+                                        <usercontrols:RawLayersViewer Layers="{Binding BitmapManager.ActiveDocument.Layers}"
+                                                                      Structure="{Binding BitmapManager.ActiveDocument.LayerStructure}"/>
                                     </LayoutAnchorable>
                                 </LayoutAnchorablePane>
                                 <LayoutAnchorablePane>
                                     <LayoutAnchorable ContentId="navigation" Title="Navigation" 
                                                       CanHide="True" CanAutoHide="False"
                                                       CanDockAsTabbedDocument="False" CanFloat="True">
-                                        <usercontrols:PreviewWindow Document="{Binding BitmapManager.ActiveDocument}"
-                                                                    PrimaryColor="{Binding ColorsSubViewModel.PrimaryColor, Mode=TwoWay}"/>
+                                        <usercontrols:PreviewWindow 
+                                            Document="{Binding BitmapManager.ActiveDocument}"
+                                            PrimaryColor="{Binding ColorsSubViewModel.PrimaryColor, Mode=TwoWay}"/>
                                     </LayoutAnchorable>
                                 </LayoutAnchorablePane>
                             </LayoutAnchorablePaneGroup>
@@ -404,13 +328,14 @@
             </Grid>
         </Grid>
 
-        <StackPanel Orientation="Vertical" Cursor="Arrow" Grid.Row="2" Grid.Column="0"
-                    Background="{StaticResource AccentColor}" Grid.RowSpan="2">
+        <Border Grid.Row="2" Grid.Column="0"
+                    Background="{StaticResource AccentColor}" Grid.RowSpan="2" CornerRadius="5">
+        <StackPanel Orientation="Vertical" Cursor="Arrow" >
 
             <ItemsControl ItemsSource="{Binding ToolsSubViewModel.ToolSet}">
                 <ItemsControl.ItemTemplate>
                     <DataTemplate>
-                        <Button BorderBrush="White"
+                        <Button BorderBrush="White"                                
                                 BorderThickness="{Binding IsActive, Converter={StaticResource BoolToIntConverter}}"
                                 Style="{StaticResource ToolButtonStyle}"
                                 Command="{Binding Path=DataContext.ToolsSubViewModel.SelectToolCommand,
@@ -419,11 +344,17 @@
                             <Button.Background>
                                 <ImageBrush ImageSource="{Binding ImagePath}" Stretch="Uniform" />
                             </Button.Background>
+                                <Button.Resources>
+                                    <Style TargetType="Border">
+                                        <Setter Property="CornerRadius" Value="2.5"/>
+                                    </Style>
+                                </Button.Resources>
                         </Button>
                     </DataTemplate>
                 </ItemsControl.ItemTemplate>
             </ItemsControl>
         </StackPanel>
+        </Border>
 
         <Grid Grid.Row="3" Grid.Column="1">
             <Grid.ColumnDefinitions>

+ 180 - 180
PixiEditor/Views/MainWindow.xaml.cs

@@ -1,180 +1,180 @@
-using Microsoft.Extensions.DependencyInjection;
-using PixiEditor.Models.UserPreferences;
-using PixiEditor.ViewModels;
-using System;
-using System.ComponentModel;
-using System.Windows;
-using System.Windows.Input;
-using PixiEditor.ViewModels.SubViewModels.Main;
-using System.Diagnostics;
-using System.Linq;
-using PixiEditor.Views.Dialogs;
-using System.Windows.Media.Imaging;
-using PixiEditor.Models.DataHolders;
-using System.Windows.Interop;
-
-namespace PixiEditor
-{
-    /// <summary>
-    ///     Interaction logic for MainWindow.xaml.
-    /// </summary>
-    public partial class MainWindow : Window
-    {
-        private static WriteableBitmap pixiEditorLogo;
-
-        private PreferencesSettings preferences;
-
-        public new ViewModelMain DataContext { get => (ViewModelMain)base.DataContext; set => base.DataContext = value; }
-
-        public MainWindow()
-        {
-            preferences = new PreferencesSettings();
-
-            IServiceCollection services = new ServiceCollection()
-                .AddSingleton<IPreferences>(preferences)
-                .AddSingleton<StylusViewModel>()
-                .AddSingleton<WindowViewModel>();
-
-            DataContext = new ViewModelMain(services.BuildServiceProvider());
-
-            InitializeComponent();
-
-            pixiEditorLogo = BitmapFactory.FromResource(@"/Images/PixiEditorLogo.png");
-
-            UpdateWindowChromeBorderThickness();
-            StateChanged += MainWindow_StateChanged;
-            Activated += MainWindow_Activated;
-
-            DataContext.CloseAction = Close;
-            Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
-
-            DataContext.BitmapManager.DocumentChanged += BitmapManager_DocumentChanged;
-            preferences.AddCallback<bool>("ImagePreviewInTaskbar", x =>
-            {
-                if (x)
-                {
-                    UpdateTaskbarIcon(DataContext.BitmapManager.ActiveDocument);
-                }
-                else
-                {
-                    UpdateTaskbarIcon(null);
-                }
-            });
-        }
-
-        protected override void OnClosing(CancelEventArgs e)
-        {
-            DataContext.CloseWindow(e);
-            DataContext.DiscordViewModel.Dispose();
-        }
-
-        protected override void OnSourceInitialized(EventArgs e)
-        {
-            base.OnSourceInitialized(e);
-            ((HwndSource)PresentationSource.FromVisual(this)).AddHook(Helpers.WindowSizeHelper.SetMaxSizeHook);
-        }
-
-        [Conditional("RELEASE")]
-        private static void CloseHelloThereIfRelease()
-        {
-            Application.Current.Windows.OfType<HelloTherePopup>().ToList().ForEach(x => { if (!x.IsClosing) x.Close(); });
-        }
-
-        private void BitmapManager_DocumentChanged(object sender, Models.Events.DocumentChangedEventArgs e)
-        {
-            if (preferences.GetPreference("ImagePreviewInTaskbar", false))
-            {
-                UpdateTaskbarIcon(e.NewDocument);
-            }
-        }
-
-        private void UpdateTaskbarIcon(Document document)
-        {
-            if (document?.PreviewImage == null)
-            {
-                Icon = pixiEditorLogo;
-                return;
-            }
-
-            var previewCopy = document.PreviewImage.Clone()
-                .Resize(512, 512, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
-
-            previewCopy.Blit(new Rect(256, 256, 256, 256), pixiEditorLogo, new Rect(0, 0, 512, 512));
-
-            Icon = previewCopy;
-        }
-
-        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
-        {
-            e.CanExecute = true;
-        }
-
-        private void CommandBinding_Executed_Minimize(object sender, ExecutedRoutedEventArgs e)
-        {
-            SystemCommands.MinimizeWindow(this);
-        }
-
-        private void CommandBinding_Executed_Maximize(object sender, ExecutedRoutedEventArgs e)
-        {
-            SystemCommands.MaximizeWindow(this);
-        }
-
-        private void CommandBinding_Executed_Restore(object sender, ExecutedRoutedEventArgs e)
-        {
-            SystemCommands.RestoreWindow(this);
-        }
-
-        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
-        {
-            SystemCommands.CloseWindow(this);
-        }
-
-        private void MainWindow_Activated(object sender, EventArgs e)
-        {
-            CloseHelloThereIfRelease();
-        }
-
-        private void UpdateWindowChromeBorderThickness()
-        {
-            if (WindowState == WindowState.Maximized)
-            {
-                windowsChrome.ResizeBorderThickness = new Thickness(0, 0, 0, 0);
-            }
-            else
-            {
-                windowsChrome.ResizeBorderThickness = new Thickness(5, 5, 5, 5);
-            }
-        }
-
-        private void MainWindow_StateChanged(object sender, EventArgs e)
-        {
-            UpdateWindowChromeBorderThickness();
-
-            if (WindowState == WindowState.Maximized)
-            {
-                RestoreButton.Visibility = Visibility.Visible;
-                MaximizeButton.Visibility = Visibility.Collapsed;
-            }
-            else
-            {
-                RestoreButton.Visibility = Visibility.Collapsed;
-                MaximizeButton.Visibility = Visibility.Visible;
-            }
-        }
-
-        private void MainWindow_Initialized(object sender, EventArgs e)
-        {
-            AppDomain.CurrentDomain.UnhandledException += (sender, e) => Helpers.CrashHelper.SaveCrashInfo((Exception)e.ExceptionObject);
-        }
-
-        private void MainWindow_Drop(object sender, DragEventArgs e)
-        {
-            if (e.Data.GetDataPresent(DataFormats.FileDrop))
-            {
-                string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
-
-                DataContext.FileSubViewModel.Open(files[0]);
-            }
-        }
-    }
-}
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels;
+using System;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Input;
+using PixiEditor.ViewModels.SubViewModels.Main;
+using System.Diagnostics;
+using System.Linq;
+using PixiEditor.Views.Dialogs;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.DataHolders;
+using System.Windows.Interop;
+
+namespace PixiEditor
+{
+    /// <summary>
+    ///     Interaction logic for MainWindow.xaml.
+    /// </summary>
+    public partial class MainWindow : Window
+    {
+        private static WriteableBitmap pixiEditorLogo;
+
+        private PreferencesSettings preferences;
+
+        public new ViewModelMain DataContext { get => (ViewModelMain)base.DataContext; set => base.DataContext = value; }
+
+        public MainWindow()
+        {
+            preferences = new PreferencesSettings();
+
+            IServiceCollection services = new ServiceCollection()
+                .AddSingleton<IPreferences>(preferences)
+                .AddSingleton<StylusViewModel>()
+                .AddSingleton<WindowViewModel>();
+
+            DataContext = new ViewModelMain(services.BuildServiceProvider());
+
+            InitializeComponent();
+
+            pixiEditorLogo = BitmapFactory.FromResource(@"/Images/PixiEditorLogo.png");
+
+            UpdateWindowChromeBorderThickness();
+            StateChanged += MainWindow_StateChanged;
+            Activated += MainWindow_Activated;
+
+            DataContext.CloseAction = Close;
+            Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
+
+            DataContext.BitmapManager.DocumentChanged += BitmapManager_DocumentChanged;
+            preferences.AddCallback<bool>("ImagePreviewInTaskbar", x =>
+            {
+                if (x)
+                {
+                    UpdateTaskbarIcon(DataContext.BitmapManager.ActiveDocument);
+                }
+                else
+                {
+                    UpdateTaskbarIcon(null);
+                }
+            });
+        }
+
+        protected override void OnClosing(CancelEventArgs e)
+        {
+            DataContext.CloseWindow(e);
+            DataContext.DiscordViewModel.Dispose();
+        }
+
+        protected override void OnSourceInitialized(EventArgs e)
+        {
+            base.OnSourceInitialized(e);
+            ((HwndSource)PresentationSource.FromVisual(this)).AddHook(Helpers.WindowSizeHelper.SetMaxSizeHook);
+        }
+
+        [Conditional("RELEASE")]
+        private static void CloseHelloThereIfRelease()
+        {
+            Application.Current.Windows.OfType<HelloTherePopup>().ToList().ForEach(x => { if (!x.IsClosing) x.Close(); });
+        }
+
+        private void BitmapManager_DocumentChanged(object sender, Models.Events.DocumentChangedEventArgs e)
+        {
+            if (preferences.GetPreference("ImagePreviewInTaskbar", false))
+            {
+                UpdateTaskbarIcon(e.NewDocument);
+            }
+        }
+
+        private void UpdateTaskbarIcon(Document document)
+        {
+            if (document?.PreviewImage == null)
+            {
+                Icon = pixiEditorLogo;
+                return;
+            }
+
+            var previewCopy = document.PreviewImage.Clone()
+                .Resize(512, 512, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+
+            previewCopy.Blit(new Rect(256, 256, 256, 256), pixiEditorLogo, new Rect(0, 0, 512, 512));
+
+            Icon = previewCopy;
+        }
+
+        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+        {
+            e.CanExecute = true;
+        }
+
+        private void CommandBinding_Executed_Minimize(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.MinimizeWindow(this);
+        }
+
+        private void CommandBinding_Executed_Maximize(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.MaximizeWindow(this);
+        }
+
+        private void CommandBinding_Executed_Restore(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.RestoreWindow(this);
+        }
+
+        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.CloseWindow(this);
+        }
+
+        private void MainWindow_Activated(object sender, EventArgs e)
+        {
+            CloseHelloThereIfRelease();
+        }
+
+        private void UpdateWindowChromeBorderThickness()
+        {
+            if (WindowState == WindowState.Maximized)
+            {
+                windowsChrome.ResizeBorderThickness = new Thickness(0, 0, 0, 0);
+            }
+            else
+            {
+                windowsChrome.ResizeBorderThickness = new Thickness(5, 5, 5, 5);
+            }
+        }
+
+        private void MainWindow_StateChanged(object sender, EventArgs e)
+        {
+            UpdateWindowChromeBorderThickness();
+
+            if (WindowState == WindowState.Maximized)
+            {
+                RestoreButton.Visibility = Visibility.Visible;
+                MaximizeButton.Visibility = Visibility.Collapsed;
+            }
+            else
+            {
+                RestoreButton.Visibility = Visibility.Collapsed;
+                MaximizeButton.Visibility = Visibility.Visible;
+            }
+        }
+
+        private void MainWindow_Initialized(object sender, EventArgs e)
+        {
+            AppDomain.CurrentDomain.UnhandledException += (sender, e) => Helpers.CrashHelper.SaveCrashInfo((Exception)e.ExceptionObject);
+        }
+
+        private void MainWindow_Drop(object sender, DragEventArgs e)
+        {
+            if (e.Data.GetDataPresent(DataFormats.FileDrop))
+            {
+                string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
+
+                DataContext.FileSubViewModel.Open(files[0]);
+            }
+        }
+    }
+}

+ 52 - 35
PixiEditor/Views/UserControls/DrawingViewPort.xaml

@@ -1,48 +1,57 @@
-<UserControl x:Class="PixiEditor.Views.UserControls.DrawingViewPort"
+<UserControl
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
              xmlns:local="clr-namespace:PixiEditor.Views.UserControls" 
              xmlns:tools="clr-namespace:PixiEditor.Models.Tools.Tools"
-             xmlns:vws="clr-namespace:PixiEditor.Views" 
+             xmlns:vws="clr-namespace:PixiEditor.Views"
              xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
              xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" xmlns:sys="clr-namespace:System;assembly=System.Runtime" x:Class="PixiEditor.Views.UserControls.DrawingViewPort"
              mc:Ignorable="d" 
-             d:DesignHeight="450" d:DesignWidth="800" Name="uc">
+             d:DesignHeight="450" d:DesignWidth="800" x:Name="uc">
     <UserControl.Resources>
         <converters:BoolToIntConverter x:Key="BoolToIntConverter" />
         <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
         <converters:IntToViewportRectConverter x:Key="IntToViewportRectConverter" />
+        <converters:ZoomToViewportConverter x:Key="ZoomToViewportConverter"/>
     </UserControl.Resources>
-    <vws:MainDrawingPanel ZoomPercentage="{Binding ZoomPercentage, Mode=TwoWay, ElementName=uc}"
-                          Center="{Binding RecenterZoombox, Mode=TwoWay, ElementName=uc}" 
+    <vws:MainDrawingPanel ZoomPercentage="{Binding ZoomPercentage, ElementName=uc, Mode=TwoWay}"
+                          Center="{Binding RecenterZoombox, ElementName=uc, Mode=TwoWay}" 
                           x:Name="DrawingPanel"
                           CenterOnStart="True" Cursor="{Binding Cursor, ElementName=uc}" 
                           MiddleMouseClickedCommand="{Binding MiddleMouseClickedCommand, ElementName=uc}" 
                           MiddleMouseClickedCommandParameter="{x:Type tools:MoveViewportTool}"
                           ViewportPosition="{Binding ViewportPosition, ElementName=uc, Mode=TwoWay}">
-            <i:Interaction.Triggers>
-                <i:EventTrigger EventName="MouseMove">
-                    <i:InvokeCommandAction Command="{Binding MouseMoveCommand, ElementName=uc}" />
-                </i:EventTrigger>
-                <i:EventTrigger EventName="MouseDown">
-                    <i:InvokeCommandAction Command="{Binding MouseDownCommand, ElementName=uc}"/>
-                </i:EventTrigger>
-            </i:Interaction.Triggers>
-            <i:Interaction.Behaviors>
-                <behaviors:MouseBehavior RelativeTo="{Binding ElementName=DrawingPanel, Path=Item}"
-                                                  MouseX="{Binding MouseXOnCanvas, Mode=TwoWay, ElementName=uc}"
-                                                  MouseY="{Binding MouseYOnCanvas, Mode=TwoWay, ElementName=uc}" />
-            </i:Interaction.Behaviors>
+        <i:Interaction.Triggers>
+            <i:EventTrigger EventName="MouseMove">
+                <i:InvokeCommandAction Command="{Binding MouseMoveCommand, ElementName=uc}" />
+            </i:EventTrigger>
+            <i:EventTrigger EventName="MouseDown">
+                <i:InvokeCommandAction Command="{Binding MouseDownCommand, ElementName=uc}"/>
+            </i:EventTrigger>
+        </i:Interaction.Triggers>
+        <i:Interaction.Behaviors>
+            <behaviors:MouseBehavior RelativeTo="{Binding Item, ElementName=DrawingPanel}"
+                                                  MouseX="{Binding MouseXOnCanvas, ElementName=uc, Mode=TwoWay}"
+                                                  MouseY="{Binding MouseYOnCanvas, ElementName=uc, Mode=TwoWay}" />
+        </i:Interaction.Behaviors>
         <vws:MainDrawingPanel.Item>
             <Canvas Width="{Binding Width}"
                                 Height="{Binding Height}" VerticalAlignment="Center"
-                                HorizontalAlignment="Center">
-                <Image Source="/Images/transparentbg.png"
-                                   Height="{Binding Height}"
-                                   Width="{Binding Width}" Opacity="0.9"
-                                   Stretch="UniformToFill" />
+                                HorizontalAlignment="Center" RenderOptions.BitmapScalingMode="NearestNeighbor">
+                <Canvas.Background>
+                    <ImageBrush ImageSource="/Images/CheckerTile.png" TileMode="Tile" ViewportUnits="Absolute">
+                        <ImageBrush.Viewport>
+                            <Binding Path="Scale" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type xctk:Zoombox}}" Converter="{StaticResource ZoomToViewportConverter}">
+                                <Binding.ConverterParameter>
+                                    <sys:Double>16</sys:Double>
+                                </Binding.ConverterParameter>
+                            </Binding>
+                        </ImageBrush.Viewport>
+                    </ImageBrush>
+                </Canvas.Background>
                 <Image Source="{Binding PreviewLayer.LayerBitmap}" Panel.ZIndex="2"
                                    RenderOptions.BitmapScalingMode="NearestNeighbor" Stretch="Uniform"
                                    Width="{Binding PreviewLayer.Width}"
@@ -57,10 +66,21 @@
                     <ItemsControl.ItemTemplate>
                         <DataTemplate>
                             <Image VerticalAlignment="Top" HorizontalAlignment="Left" Source="{Binding LayerBitmap}"
-                                               Visibility="{Binding IsVisible, Converter={StaticResource BoolToVisibilityConverter}}"
                                                RenderOptions.BitmapScalingMode="NearestNeighbor" Stretch="Uniform"
-                                               Opacity="{Binding Opacity}"
-                                               Width="{Binding Width}" Height="{Binding Height}" Margin="{Binding Offset}" />
+                                               Width="{Binding Width}" Height="{Binding Height}" Margin="{Binding Offset}">
+                                <Image.Visibility>
+                                    <MultiBinding Converter="{converters:FinalIsVisibleToVisiblityConverter}">
+                                        <Binding Path="."/>
+                                        <Binding Path="IsVisible"/>
+                                    </MultiBinding>
+                                </Image.Visibility>
+                                <Image.Opacity>
+                                    <MultiBinding Converter="{converters:LayerToFinalOpacityConverter}">
+                                        <Binding Path="."/>
+                                        <Binding Path="Opacity"/>
+                                    </MultiBinding>
+                                </Image.Opacity>
+                            </Image>
                         </DataTemplate>
                     </ItemsControl.ItemTemplate>
                 </ItemsControl>
@@ -71,8 +91,7 @@
                                    Height="{Binding ActiveSelection.SelectionLayer.Height}" 
                                    Margin="{Binding ActiveSelection.SelectionLayer.Offset}" />
                 <Grid ShowGridLines="True" Width="{Binding Width}" Height="{Binding Height}" Panel.ZIndex="10" 
-                      Visibility="{Binding GridLinesVisible, Converter={StaticResource BoolToVisibilityConverter},
-                    RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:DrawingViewPort}}}">
+                      Visibility="{Binding GridLinesVisible, Converter={StaticResource BoolToVisibilityConverter}, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:DrawingViewPort}}}">
                     <Rectangle Focusable="False">
                         <Rectangle.Fill>
                             <VisualBrush Viewport="{Binding Height, Converter={StaticResource IntToViewportRectConverter}}" ViewboxUnits="Absolute" TileMode="Tile" >
@@ -84,8 +103,7 @@
                     </Rectangle>
                     <Rectangle Focusable="False">
                         <Rectangle.Fill>
-                            <VisualBrush Viewport="{Binding Width, Converter={StaticResource IntToViewportRectConverter}, 
-                            ConverterParameter=vertical}" ViewboxUnits="Absolute" TileMode="Tile" >
+                            <VisualBrush Viewport="{Binding Width, Converter={StaticResource IntToViewportRectConverter}, ConverterParameter=vertical}" ViewboxUnits="Absolute" TileMode="Tile" >
                                 <VisualBrush.Visual>
                                     <Line  X1="0" Y1="0" X2="0" Y2="1" Stroke="Black" StrokeThickness="0.01"/>
                                 </VisualBrush.Visual>
@@ -96,17 +114,16 @@
                         <Rectangle.Fill>
                             <VisualBrush Viewport="{Binding Height, Converter={StaticResource IntToViewportRectConverter}}" ViewboxUnits="Absolute" TileMode="Tile" >
                                 <VisualBrush.Visual>
-                                    <Line  X1="0" Y1="0" X2="1" Y2="0" Stroke="White" StrokeThickness="0.01"/>
+                                    <Line  X1="0" Y1="0" X2="1" Y2="0" Stroke="White" StrokeThickness="0.02"/>
                                 </VisualBrush.Visual>
                             </VisualBrush>
                         </Rectangle.Fill>
                     </Rectangle>
                     <Rectangle Focusable="False">
                         <Rectangle.Fill>
-                            <VisualBrush Viewport="{Binding Width, Converter={StaticResource IntToViewportRectConverter}, 
-                            ConverterParameter=vertical}" ViewboxUnits="Absolute" TileMode="Tile" >
+                            <VisualBrush Viewport="{Binding Width, Converter={StaticResource IntToViewportRectConverter}, ConverterParameter=vertical}" ViewboxUnits="Absolute" TileMode="Tile" >
                                 <VisualBrush.Visual>
-                                    <Line  X1="0" Y1="0" X2="0" Y2="1" Stroke="White" StrokeThickness="0.01"/>
+                                    <Line  X1="0" Y1="0" X2="0" Y2="1" Stroke="White" StrokeThickness="0.02"/>
                                 </VisualBrush.Visual>
                             </VisualBrush>
                         </Rectangle.Fill>
@@ -114,5 +131,5 @@
                 </Grid>
             </Canvas>
         </vws:MainDrawingPanel.Item>
-        </vws:MainDrawingPanel>
+    </vws:MainDrawingPanel>
 </UserControl>

+ 3 - 1
PixiEditor/Views/UserControls/DrawingViewPort.xaml.cs

@@ -1,4 +1,6 @@
-using System.Windows;
+using System;
+using System.Diagnostics;
+using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
 

+ 67 - 0
PixiEditor/Views/UserControls/LayerGroupControl.xaml

@@ -0,0 +1,67 @@
+<UserControl x:Class="PixiEditor.Views.UserControls.LayerGroupControl"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours" xmlns:userControls="clr-namespace:PixiEditor.Views" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" xmlns:helpers="clr-namespace:PixiEditor.Helpers.UI"
+             mc:Ignorable="d" Focusable="True"
+             d:DesignHeight="60" d:DesignWidth="250" Name="groupControl" DragEnter="GroupControl_DragEnter" DragLeave="GroupControl_DragLeave" DragDrop.Drop="GroupControl_DragLeave">
+    <UserControl.Resources>
+        <converters:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
+        <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
+    </UserControl.Resources>
+    <Border BorderThickness="0 0 0 0.5" BorderBrush="Gray" MinWidth="60" Focusable="True" Tag="{Binding ElementName=groupControl}" MouseDown="Border_MouseDown">
+        <i:Interaction.Behaviors>
+            <behaviors:ClearFocusOnClickBehavior/>
+        </i:Interaction.Behaviors>
+        <Grid>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="10"/>
+                <RowDefinition Height="15"/>
+                <RowDefinition Height="10"/>
+            </Grid.RowDefinitions>
+            <Grid AllowDrop="True" DragEnter="Grid_DragEnter" Drop="Grid_Drop_Top" DragLeave="Grid_DragLeave" Grid.Row="0" Grid.ColumnSpan="3" Background="Transparent" Panel.ZIndex="3"/>
+            <Grid Visibility="Collapsed" x:Name="middleDropGrid" Grid.Row="1" AllowDrop="True" Panel.ZIndex="2" Background="Transparent" DragEnter="Grid_CenterEnter" Drop="Grid_Drop_Center" DragLeave="Grid_CenterLeave"></Grid>
+            <Grid x:Name="centerGrid" Grid.Row="0" Grid.RowSpan="3" Background="Transparent">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="30"/>
+                    <ColumnDefinition Width="*"/>
+                </Grid.ColumnDefinitions>
+                <CheckBox Style="{StaticResource ImageCheckBox}" VerticalAlignment="Center"
+                      IsThreeState="False" HorizontalAlignment="Center" Click="CheckBox_Checked"
+                      IsChecked="{Binding Path=IsVisibleUndoTriggerable, ElementName=groupControl}" Grid.Column="0" Height="16"/>
+                
+                <StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Left">
+                    <Rectangle Width="{Binding Path=(helpers:TreeViewItemHelper.Indent).Value, Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}}" Fill="Transparent" StrokeThickness="0"/>
+                
+                <StackPanel Grid.Row="1" Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left">
+                        <Border Width="30" Height="30" BorderThickness="1" BorderBrush="Black" Background="{StaticResource MainColor}"
+                           Margin="5, 0, 10, 0">
+                            <Image Source="{Binding PreviewImage, ElementName=groupControl}" Stretch="Uniform" Width="20" Height="20" 
+                       RenderOptions.BitmapScalingMode="NearestNeighbor"/>
+                        </Border>
+                        <userControls:EditableTextBlock
+                    FontSize="16"
+                    VerticalAlignment="Center"
+                    IsEditing="{Binding GroupData.IsRenaming, ElementName=groupControl, Mode=TwoWay}"
+                    Text="{Binding GroupData.Name, ElementName=groupControl, Mode=TwoWay}" />
+                </StackPanel>
+                    <Image Source="/Images/Folder.png" Height="20" Margin="0,0,10,0" HorizontalAlignment="Right"/>
+                </StackPanel>
+            </Grid>
+            <Grid DragEnter="Grid_DragEnter" Drop="Grid_Drop_Bottom"  DragLeave="Grid_DragLeave" Grid.Row="2" AllowDrop="{Binding  GroupData.IsExpanded, ElementName=groupControl, Converter={StaticResource InverseBooleanConverter}}" Grid.ColumnSpan="2" Background="Transparent"/>
+        </Grid>
+        <Border.ContextMenu>
+            <ContextMenu>
+                <MenuItem Header="Rename"
+                                     Command="{Binding PlacementTarget.Tag.LayersViewModel.RenameGroupCommand, 
+                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}" CommandParameter="{Binding PlacementTarget.Tag.GroupGuid, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
+                </MenuItem>
+                <MenuItem Header="Delete"
+                                     Command="{Binding PlacementTarget.Tag.LayersViewModel.DeleteGroupCommand, 
+                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}" CommandParameter="{Binding PlacementTarget.Tag.GroupGuid, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
+                </MenuItem>
+            </ContextMenu>
+        </Border.ContextMenu>
+    </Border>
+</UserControl>

+ 312 - 0
PixiEditor/Views/UserControls/LayerGroupControl.xaml.cs

@@ -0,0 +1,312 @@
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Undo;
+using PixiEditor.ViewModels.SubViewModels.Main;
+using System;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.Views.UserControls
+{
+    /// <summary>
+    /// Interaction logic for LayerFolder.xaml.
+    /// </summary>
+    public partial class LayerGroupControl : UserControl
+    {
+        public Guid GroupGuid
+        {
+            get { return (Guid)GetValue(GroupGuidProperty); }
+            set { SetValue(GroupGuidProperty, value); }
+        }
+
+        public const string LayerGroupControlDataName = "PixiEditor.Views.UserControls.LayerGroupControl";
+        public const string LayerContainerDataName = "PixiEditor.Views.UserControls.LayerStructureItemContainer";
+
+        public static readonly DependencyProperty GroupGuidProperty =
+            DependencyProperty.Register("GroupGuid", typeof(Guid), typeof(LayerGroupControl), new PropertyMetadata(Guid.NewGuid()));
+
+        public LayersViewModel LayersViewModel
+        {
+            get { return (LayersViewModel)GetValue(LayersViewModelProperty); }
+            set { SetValue(LayersViewModelProperty, value); }
+        }
+
+        public static readonly DependencyProperty LayersViewModelProperty =
+            DependencyProperty.Register("LayersViewModel", typeof(LayersViewModel), typeof(LayerGroupControl), new PropertyMetadata(default(LayersViewModel), LayersViewModelCallback));
+
+        public bool IsVisibleUndoTriggerable
+        {
+            get { return (bool)GetValue(IsVisibleUndoTriggerableProperty); }
+            set { SetValue(IsVisibleUndoTriggerableProperty, value); }
+        }
+
+        public static readonly DependencyProperty IsVisibleUndoTriggerableProperty =
+            DependencyProperty.Register("IsVisibleUndoTriggerable", typeof(bool), typeof(LayerGroupControl), new PropertyMetadata(true));
+
+        public float GroupOpacity
+        {
+            get { return (float)GetValue(GroupOpacityProperty); }
+            set { SetValue(GroupOpacityProperty, value); }
+        }
+
+        public static readonly DependencyProperty GroupOpacityProperty =
+            DependencyProperty.Register("GroupOpacity", typeof(float), typeof(LayerGroupControl), new PropertyMetadata(1f));
+
+
+        private static void LayersViewModelCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            LayerGroupControl control = (LayerGroupControl)d;
+            if (e.OldValue is LayersViewModel oldVm && oldVm != e.NewValue)
+            {
+                oldVm.Owner.BitmapManager.MouseController.StoppedRecordingChanges -= control.MouseController_StoppedRecordingChanges;
+            }
+
+            if (e.NewValue is LayersViewModel vm)
+            {
+                vm.Owner.BitmapManager.MouseController.StoppedRecordingChanges += control.MouseController_StoppedRecordingChanges;
+            }
+        }
+
+        public string GroupName
+        {
+            get { return (string)GetValue(GroupNameProperty); }
+            set { SetValue(GroupNameProperty, value); }
+        }
+
+        public static readonly DependencyProperty GroupNameProperty =
+            DependencyProperty.Register("GroupName", typeof(string), typeof(LayerGroupControl), new PropertyMetadata(default(string)));
+
+        public GuidStructureItem GroupData
+        {
+            get { return (GuidStructureItem)GetValue(GroupDataProperty); }
+            set { SetValue(GroupDataProperty, value); }
+        }
+
+        public static readonly DependencyProperty GroupDataProperty =
+            DependencyProperty.Register("GroupData", typeof(GuidStructureItem), typeof(LayerGroupControl), new PropertyMetadata(default(GuidStructureItem), GroupDataChangedCallback));
+
+        public void GeneratePreviewImage()
+        {
+            var doc = LayersViewModel.Owner.BitmapManager.ActiveDocument;
+            var layers = doc.LayerStructure.GetGroupLayers(GroupData);
+            if (layers.Count > 0)
+            {
+                PreviewImage = BitmapUtils.GeneratePreviewBitmap(layers, doc.Width, doc.Height, 25, 25);
+            }
+        }
+
+        private static void GroupDataChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            ((LayerGroupControl)d).GeneratePreviewImage();
+        }
+
+        public WriteableBitmap PreviewImage
+        {
+            get { return (WriteableBitmap)GetValue(PreviewImageProperty); }
+            set { SetValue(PreviewImageProperty, value); }
+        }
+
+        public static readonly DependencyProperty PreviewImageProperty =
+            DependencyProperty.Register("PreviewImage", typeof(WriteableBitmap), typeof(LayerGroupControl), new PropertyMetadata(default(WriteableBitmap)));
+
+        public LayerGroupControl()
+        {
+            InitializeComponent();
+        }
+
+        private void MouseController_StoppedRecordingChanges(object sender, EventArgs e)
+        {
+            GeneratePreviewImage();
+        }
+
+        private void Grid_DragEnter(object sender, DragEventArgs e)
+        {
+            Grid item = sender as Grid;
+
+            item.Background = LayerItem.HighlightColor;
+        }
+
+        private void Grid_CenterEnter(object sender, DragEventArgs e)
+        {
+            centerGrid.Background = LayerItem.HighlightColor;
+        }
+
+        private void Grid_DragLeave(object sender, DragEventArgs e)
+        {
+            Grid grid = (Grid)sender;
+
+            LayerItem.RemoveDragEffect(grid);
+        }
+
+        private void Grid_CenterLeave(object sender, DragEventArgs e)
+        {
+            LayerItem.RemoveDragEffect(centerGrid);
+        }
+
+        private void HandleDrop(IDataObject dataObj, Grid grid, bool above)
+        {
+            Guid referenceLayer = above ? GroupData.EndLayerGuid : GroupData.StartLayerGuid;
+            LayerItem.RemoveDragEffect(grid);
+
+            if (dataObj.GetDataPresent(LayerContainerDataName))
+            {
+                HandleLayerDrop(dataObj, above, referenceLayer, false);
+            }
+
+            if (dataObj.GetDataPresent(LayerGroupControlDataName))
+            {
+                HandleGroupControlDrop(dataObj, referenceLayer, above, false);
+            }
+        }
+
+        private void HandleLayerDrop(IDataObject dataObj, bool above, Guid referenceLayer, bool putItInside) // step brother
+        {
+            var data = (LayerStructureItemContainer)dataObj.GetData(LayerContainerDataName);
+            Guid group = data.Layer.LayerGuid;
+
+            data.LayerCommandsViewModel.Owner.BitmapManager.ActiveDocument.MoveLayerInStructure(group, referenceLayer, above);
+
+            Guid? refGuid = putItInside ? GroupData?.GroupGuid : GroupData?.Parent?.GroupGuid;
+
+            data.LayerCommandsViewModel.Owner.BitmapManager.ActiveDocument.LayerStructure.AssignParent(group, refGuid);
+        }
+
+        private void HandleGroupControlDrop(IDataObject dataObj, Guid referenceLayer, bool above, bool putItInside) // daddy
+        {
+            var data = (LayerGroupControl)dataObj.GetData(LayerGroupControlDataName);
+            var document = data.LayersViewModel.Owner.BitmapManager.ActiveDocument;
+
+            Guid group = data.GroupGuid;
+
+            if (group == GroupGuid || document.LayerStructure.IsChildOf(GroupData, data.GroupData))
+            {
+                return;
+            }
+
+            int modifier = above ? 1 : 0;
+
+            Layer layer = document.Layers.First(x => x.LayerGuid == referenceLayer);
+            int indexOfReferenceLayer = Math.Clamp(document.Layers.IndexOf(layer) + modifier, 0, document.Layers.Count);
+            MoveGroupWithTempLayer(above, document, group, indexOfReferenceLayer, putItInside);
+        }
+
+        private void MoveGroupWithTempLayer(bool above, Models.DataHolders.Document document, Guid group, int indexOfReferenceLayer, bool putItInside) // ¯\_(ツ)_/¯
+        {
+            // The trick here is to insert a temp layer, assign group to it, then delete it.
+            Layer tempLayer = new("_temp");
+            document.Layers.Insert(indexOfReferenceLayer, tempLayer);
+
+            Guid? refGuid = putItInside ? GroupData?.GroupGuid : GroupData?.Parent?.GroupGuid;
+
+            document.LayerStructure.AssignParent(tempLayer.LayerGuid, refGuid);
+            document.MoveGroupInStructure(group, tempLayer.LayerGuid, above);
+            document.LayerStructure.AssignParent(tempLayer.LayerGuid, null);
+            document.RemoveLayer(tempLayer, false);
+        }
+
+        private void HandleDropInside(IDataObject dataObj, Grid grid)
+        {
+            Guid referenceLayer = GroupData.EndLayerGuid;
+            LayerItem.RemoveDragEffect(grid);
+
+            if (dataObj.GetDataPresent(LayerContainerDataName))
+            {
+                HandleLayerDrop(dataObj, true, referenceLayer, true);
+            }
+
+            if (dataObj.GetDataPresent(LayerGroupControlDataName))
+            {
+                HandleGroupControlDrop(dataObj, referenceLayer, true, true);
+            }
+        }
+
+        private void Grid_Drop_Top(object sender, DragEventArgs e)
+        {
+            HandleDrop(e.Data, (Grid)sender, true);
+        }
+
+        private void Grid_Drop_Center(object sender, DragEventArgs e)
+        {
+            HandleDropInside(e.Data, (Grid)sender);
+            LayerItem.RemoveDragEffect(centerGrid);
+        }
+
+        private void Grid_Drop_Bottom(object sender, DragEventArgs e)
+        {
+            HandleDrop(e.Data, (Grid)sender, false);
+        }
+
+        private void Border_MouseDown(object sender, MouseButtonEventArgs e)
+        {
+            var doc = LayersViewModel.Owner.BitmapManager.ActiveDocument;
+            var layer = doc.Layers.First(x => x.LayerGuid == GroupData.EndLayerGuid);
+            if (doc.ActiveLayerGuid != layer.LayerGuid)
+            {
+                doc.SetMainActiveLayer(doc.Layers.IndexOf(layer));
+            }
+        }
+
+        private void CheckBox_Checked(object sender, RoutedEventArgs e)
+        {
+            HandleCheckboxChange(((CheckBox)e.OriginalSource).IsChecked.Value);
+        }
+
+        private void HandleCheckboxChange(bool value)
+        {
+            if (LayersViewModel?.Owner?.BitmapManager?.ActiveDocument != null)
+            {
+                var doc = LayersViewModel.Owner.BitmapManager.ActiveDocument;
+
+                IsVisibleUndoTriggerable = value;
+
+                var processArgs = new object[] { GroupGuid, value };
+                var reverseProcessArgs = new object[] { GroupGuid, !value };
+
+                ChangeGroupVisibilityProcess(processArgs);
+
+                doc.UndoManager.AddUndoChange(
+                new Change(
+                    ChangeGroupVisibilityProcess,
+                    reverseProcessArgs,
+                    ChangeGroupVisibilityProcess,
+                    processArgs,
+                    $"Change {GroupName} visibility"), false);
+            }
+        }
+
+        private void ChangeGroupVisibilityProcess(object[] args)
+        {
+            var doc = LayersViewModel.Owner.BitmapManager.ActiveDocument;
+            if (args.Length == 2 &&
+                args[0] is Guid groupGuid &&
+                args[1] is bool value
+                && doc != null)
+            {
+                var group = doc.LayerStructure.GetGroupByGuid(groupGuid);
+
+                group.IsVisible = value;
+                var layers = doc.LayerStructure.GetGroupLayers(group);
+
+                foreach (var layer in layers)
+                {
+                    layer.RaisePropertyChange(nameof(layer.IsVisible));
+                }
+
+                IsVisibleUndoTriggerable = value;
+            }
+        }
+
+        private void GroupControl_DragEnter(object sender, DragEventArgs e)
+        {
+            middleDropGrid.Visibility = Visibility.Visible;
+        }
+
+        private void GroupControl_DragLeave(object sender, DragEventArgs e)
+        {
+            middleDropGrid.Visibility = Visibility.Collapsed;
+        }
+    }
+}

+ 28 - 23
PixiEditor/Views/UserControls/LayerItem.xaml

@@ -4,8 +4,7 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
              xmlns:local="clr-namespace:PixiEditor.Views"
-             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
-             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
+             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours" xmlns:layers="clr-namespace:PixiEditor.Models.Layers" xmlns:helpers="clr-namespace:PixiEditor.Helpers.UI"
              mc:Ignorable="d" Focusable="True"
              d:DesignHeight="60" d:DesignWidth="250" Name="uc"
              MouseLeave="LayerItem_OnMouseLeave" MouseEnter="LayerItem_OnMouseEnter">
@@ -22,32 +21,38 @@
             </i:EventTrigger>
         </i:Interaction.Triggers>
         <Grid>
-            <Grid.ColumnDefinitions>
-                <ColumnDefinition Width="30"/>
-                <ColumnDefinition Width="199*"/>
-                <ColumnDefinition Width="20"/>
-            </Grid.ColumnDefinitions>
-            <CheckBox Style="{StaticResource ImageCheckBox}" VerticalAlignment="Center"
+            <Grid.RowDefinitions>
+                <RowDefinition Height="10"/>
+                <RowDefinition Height="25"/>
+
+            </Grid.RowDefinitions>
+            <Grid AllowDrop="True" DragEnter="Grid_DragEnter" Drop="Grid_Drop_Top" DragLeave="Grid_DragLeave" Grid.Row="0" Grid.ColumnSpan="3" Background="Transparent"/>
+            <Grid Grid.Row="1" Grid.RowSpan="3" Margin="0,-10,0,0" VerticalAlignment="Center" AllowDrop="False">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="30"/>
+                    <ColumnDefinition Width="*"/>
+                </Grid.ColumnDefinitions>
+                <CheckBox Style="{StaticResource ImageCheckBox}" VerticalAlignment="Center"
                       IsThreeState="False" HorizontalAlignment="Center" 
                       IsChecked="{Binding Path=IsVisibleUndoTriggerable, Mode=TwoWay}" Grid.Column="0" Height="16" />
-            <StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Left" Margin="5,0,0,0">
-                <Image Source="{Binding PreviewImage,ElementName=uc}" Stretch="Uniform" Width="50" Height="20" Margin="0,0,20,0"
+                <StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Left">
+                    <Rectangle Width="{Binding Path=(helpers:TreeViewItemHelper.Indent).Value, Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}}" Fill="Transparent" StrokeThickness="0"/>
+                    <Border Width="30" Height="30" BorderThickness="1" BorderBrush="Black" Background="{StaticResource MainColor}"
+                           Margin="5, 0, 10, 0">
+                        <Image Source="{Binding PreviewImage,ElementName=uc}" Stretch="Uniform" Width="20" Height="20" 
                        RenderOptions.BitmapScalingMode="NearestNeighbor"/>
-                <local:EditableTextBlock
-                    IsEditing="{Binding IsRenaming, ElementName=uc, Mode=TwoWay}" FontSize="16"
+                    </Border>
+
+                    <local:EditableTextBlock
+                    IsEditing="{Binding IsRenaming, ElementName=uc, Mode=TwoWay}" FontSize="15"
                     VerticalAlignment="Center"
                     Text="{Binding LayerName, ElementName=uc, Mode=TwoWay}" />
-            </StackPanel>
-            <StackPanel Visibility="{Binding Path=ControlButtonsVisible, ElementName=uc}" 
-                        Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Width="11" 
-                        Grid.Column="2">
-                <Button CommandParameter="{Binding LayerIndex, ElementName=uc}" Command="{Binding Path=MoveToFrontCommand, ElementName=uc}" Background="Transparent" Style="{StaticResource OpacityButtonStyle}" Foreground="White" HorizontalAlignment="Center" BorderThickness="0">
-                    <TextBlock Text="&#9650;"/>
-                </Button>
-                <Button CommandParameter="{Binding LayerIndex, ElementName=uc}" Command="{Binding Path=MoveToBackCommand, ElementName=uc}" Background="Transparent" HorizontalAlignment="Center" Style="{StaticResource OpacityButtonStyle}" Foreground="White" BorderThickness="0">
-                    <TextBlock Text="&#9660;"/>
-                </Button>
-            </StackPanel>
+                </StackPanel>
+                <Grid Margin="0, 0, 0, -2.5" DragEnter="Grid_DragEnter" VerticalAlignment="Bottom" Height="10" Drop="Grid_Drop_Below"  DragLeave="Grid_DragLeave" Grid.Row="2" Grid.Column="0" AllowDrop="True"  Background="Transparent" Name="dropBelowGrid"/>
+                <Grid Margin="0, 0, 0, -2.5" VerticalAlignment="Bottom" Height="10" Grid.Row="2" Grid.Column="1" Background="{Binding ElementName=dropBelowGrid, Path=Background}"/>
+
+                <Grid Margin="0, 0, 0, -2.5" DragEnter="Grid_DragEnter" VerticalAlignment="Bottom" Height="10" Drop="Grid_Drop_Bottom" DragLeave="Grid_DragLeave" Grid.Row="2" Grid.Column="2" AllowDrop="True"  Background="Transparent"/>
+            </Grid>
         </Grid>
     </Border>
 </UserControl>

+ 92 - 17
PixiEditor/Views/UserControls/LayerItem.xaml.cs

@@ -1,24 +1,22 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Views.UserControls;
+using System;
 using System.Windows;
 using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
-using System.Windows.Navigation;
-using System.Windows.Shapes;
-using PixiEditor.Helpers;
 
 namespace PixiEditor.Views
 {
     /// <summary>
-    /// Interaction logic for LayerItem.xaml
+    /// Interaction logic for LayerItem.xaml.
     /// </summary>
     public partial class LayerItem : UserControl
     {
+        public static Brush HighlightColor = (SolidColorBrush)new BrushConverter().ConvertFrom(Document.SecondarySelectedLayerColor);
+
         public LayerItem()
         {
             InitializeComponent();
@@ -29,7 +27,7 @@ namespace PixiEditor.Views
 
         public bool IsRenaming
         {
-            get { return (bool) GetValue(IsRenamingProperty); }
+            get { return (bool)GetValue(IsRenamingProperty); }
             set { SetValue(IsRenamingProperty, value); }
         }
 
@@ -38,7 +36,7 @@ namespace PixiEditor.Views
 
         public bool IsActive
         {
-            get { return (bool) GetValue(IsActiveProperty); }
+            get { return (bool)GetValue(IsActiveProperty); }
             set { SetValue(IsActiveProperty, value); }
         }
 
@@ -56,7 +54,7 @@ namespace PixiEditor.Views
 
         public int LayerIndex
         {
-            get { return (int) GetValue(LayerIndexProperty); }
+            get { return (int)GetValue(LayerIndexProperty); }
             set { SetValue(LayerIndexProperty, value); }
         }
 
@@ -65,10 +63,19 @@ namespace PixiEditor.Views
 
         public string LayerName
         {
-            get { return (string) GetValue(LayerNameProperty); }
+            get { return (string)GetValue(LayerNameProperty); }
             set { SetValue(LayerNameProperty, value); }
         }
 
+        public Guid LayerGuid
+        {
+            get { return (Guid)GetValue(LayerGuidProperty); }
+            set { SetValue(LayerGuidProperty, value); }
+        }
+
+        public static readonly DependencyProperty LayerGuidProperty =
+            DependencyProperty.Register("LayerGuid", typeof(Guid), typeof(LayerItem), new PropertyMetadata(default(Guid)));
+
         public static readonly DependencyProperty ControlButtonsVisibleProperty = DependencyProperty.Register(
             "ControlButtonsVisible", typeof(Visibility), typeof(LayerItem), new PropertyMetadata(System.Windows.Visibility.Hidden));
 
@@ -78,7 +85,6 @@ namespace PixiEditor.Views
             set { SetValue(PreviewImageProperty, value); }
         }
 
-        // Using a DependencyProperty as the backing store for PreviewImage.  This enables animation, styling, binding, etc...
         public static readonly DependencyProperty PreviewImageProperty =
             DependencyProperty.Register("PreviewImage", typeof(WriteableBitmap), typeof(LayerItem), new PropertyMetadata(null));
 
@@ -88,7 +94,6 @@ namespace PixiEditor.Views
             set { SetValue(LayerColorProperty, value); }
         }
 
-        // Using a DependencyProperty as the backing store for LayerColor.  This enables animation, styling, binding, etc...
         public static readonly DependencyProperty LayerColorProperty =
             DependencyProperty.Register("LayerColor", typeof(string), typeof(LayerItem), new PropertyMetadata("#00000000"));
 
@@ -104,7 +109,6 @@ namespace PixiEditor.Views
             set { SetValue(MoveToBackCommandProperty, value); }
         }
 
-        // Using a DependencyProperty as the backing store for MoveToBackCommand.  This enables animation, styling, binding, etc...
         public static readonly DependencyProperty MoveToBackCommandProperty =
             DependencyProperty.Register("MoveToBackCommand", typeof(RelayCommand), typeof(LayerItem), new PropertyMetadata(default(RelayCommand)));
 
@@ -117,6 +121,11 @@ namespace PixiEditor.Views
             set { SetValue(MoveToFrontCommandProperty, value); }
         }
 
+        public static void RemoveDragEffect(Grid grid)
+        {
+            grid.Background = Brushes.Transparent;
+        }
+
         private void LayerItem_OnMouseEnter(object sender, MouseEventArgs e)
         {
             ControlButtonsVisible = Visibility.Visible;
@@ -127,5 +136,71 @@ namespace PixiEditor.Views
             ControlButtonsVisible = Visibility.Hidden;
 
         }
+
+        private void Grid_DragEnter(object sender, DragEventArgs e)
+        {
+            Grid item = sender as Grid;
+
+            item.Background = HighlightColor;
+        }
+
+        private void Grid_DragLeave(object sender, DragEventArgs e)
+        {
+            Grid item = sender as Grid;
+
+            RemoveDragEffect(item);
+        }
+
+        private void HandleGridDrop(object sender, DragEventArgs e, bool above, bool dropInParentFolder = false)
+        {
+            Grid item = sender as Grid;
+            RemoveDragEffect(item);
+
+            if (e.Data.GetDataPresent("PixiEditor.Views.UserControls.LayerStructureItemContainer"))
+            {
+                var data = (LayerStructureItemContainer)e.Data.GetData("PixiEditor.Views.UserControls.LayerStructureItemContainer");
+                Guid layer = data.Layer.LayerGuid;
+                var doc = data.LayerCommandsViewModel.Owner.BitmapManager.ActiveDocument;
+
+                doc.MoveLayerInStructure(layer, LayerGuid, above);
+                if (dropInParentFolder)
+                {
+                    Guid? groupGuid = doc.LayerStructure.GetGroupByLayer(layer)?.Parent?.GroupGuid;
+                    doc.LayerStructure.AssignParent(layer, groupGuid);
+                }
+            }
+
+            if (e.Data.GetDataPresent("PixiEditor.Views.UserControls.LayerGroupControl"))
+            {
+                var data = (LayerGroupControl)e.Data.GetData("PixiEditor.Views.UserControls.LayerGroupControl");
+                Guid folder = data.GroupGuid;
+
+                var document = data.LayersViewModel.Owner.BitmapManager.ActiveDocument;
+
+                var parentGroup = document.LayerStructure.GetGroupByLayer(LayerGuid);
+
+                if (parentGroup == data.GroupData || document.LayerStructure.IsChildOf(parentGroup, data.GroupData))
+                {
+                    return;
+                }
+
+                document.MoveGroupInStructure(folder, LayerGuid, above);
+            }
+        }
+
+        private void Grid_Drop_Top(object sender, DragEventArgs e)
+        {
+            HandleGridDrop(sender, e, true);
+        }
+
+        private void Grid_Drop_Bottom(object sender, DragEventArgs e)
+        {
+            HandleGridDrop(sender, e, false);
+        }
+
+        private void Grid_Drop_Below(object sender, DragEventArgs e)
+        {
+            HandleGridDrop(sender, e, false, true);
+        }
     }
-}
+}

+ 56 - 0
PixiEditor/Views/UserControls/LayerStructureItemContainer.xaml

@@ -0,0 +1,56 @@
+<UserControl x:Class="PixiEditor.Views.UserControls.LayerStructureItemContainer"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:vws="clr-namespace:PixiEditor.Views" xmlns:layers="clr-namespace:PixiEditor.Models.Layers" d:DataContext="{d:DesignInstance Type=layers:Layer}"
+             mc:Ignorable="d"
+             d:DesignHeight="60" d:DesignWidth="250" Name="layerStructureContainer">
+    <vws:LayerItem Tag="{Binding ElementName=layerStructureContainer}"
+                               SetActiveLayerCommand="{Binding LayerCommandsViewModel.SetActiveLayerCommand, ElementName=layerStructureContainer}"
+                                       LayerName="{Binding Name, Mode=TwoWay}" 
+                                       IsActive="{Binding IsActive, Mode=TwoWay}"
+                                       IsRenaming="{Binding IsRenaming, Mode=TwoWay}"
+                                       PreviewImage="{Binding LayerBitmap}"
+                                       LayerGuid="{Binding LayerGuid}"
+                                       LayerColor="{Binding LayerHighlightColor}"
+                                       LayerIndex="{Binding ContainerIndex, ElementName=layerStructureContainer}"
+                                      MoveToBackCommand="{Binding LayerCommandsViewModel.MoveToBackCommand, ElementName=layerStructureContainer}"
+                                      MoveToFrontCommand="{Binding LayerCommandsViewModel.MoveToFrontCommand, ElementName=layerStructureContainer}">
+        <vws:LayerItem.ContextMenu>
+            <ContextMenu>
+                <MenuItem Header="Delete"
+                                         Command="{Binding PlacementTarget.Tag.LayerCommandsViewModel.DeleteLayersCommand, 
+                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}" 
+                          CommandParameter="{Binding PlacementTarget.Tag.ContainerIndex, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
+                </MenuItem>
+                <MenuItem Header="Rename"
+                                     Command="{Binding PlacementTarget.Tag.LayerCommandsViewModel.RenameLayerCommand, 
+                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}" CommandParameter="{Binding PlacementTarget.Tag.ContainerIndex, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
+                </MenuItem>
+                <MenuItem Header="Move to front"
+                                     Command="{Binding PlacementTarget.Tag.LayerCommandsViewModel.MoveToFrontCommand, 
+                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"
+                           CommandParameter="{Binding PlacementTarget.Tag.ContainerIndex, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
+                </MenuItem>
+                <MenuItem Header="Move to back"
+                                    Command="{Binding PlacementTarget.Tag.LayerCommandsViewModel.MoveToBackCommand, 
+                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}" 
+                           CommandParameter="{Binding PlacementTarget.Tag.ContainerIndex, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
+                </MenuItem>
+                <Separator/>
+                <MenuItem Header="Merge selected"
+                                     Command="{Binding PlacementTarget.Tag.LayerCommandsViewModel.MergeSelectedCommand, 
+                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
+                <MenuItem Header="Merge with above"
+                                     Command="{Binding PlacementTarget.Tag.LayerCommandsViewModel.MergeWithAboveCommand, 
+                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}" CommandParameter="{Binding PlacementTarget.Tag.ContainerIndex, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
+                </MenuItem>
+                <MenuItem Header="Merge with below"
+                                    Command="{Binding PlacementTarget.Tag.LayerCommandsViewModel.MergeWithBelowCommand, 
+                                            RelativeSource={RelativeSource AncestorType=ContextMenu}}" CommandParameter="{Binding PlacementTarget.Tag.ContainerIndex, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
+                </MenuItem>
+            </ContextMenu>
+        </vws:LayerItem.ContextMenu>
+    </vws:LayerItem>
+</UserControl>

+ 48 - 0
PixiEditor/Views/UserControls/LayerStructureItemContainer.xaml.cs

@@ -0,0 +1,48 @@
+using System.Windows;
+using System.Windows.Controls;
+using PixiEditor.Models.Layers;
+using PixiEditor.ViewModels.SubViewModels.Main;
+
+namespace PixiEditor.Views.UserControls
+{
+    /// <summary>
+    /// Interaction logic for LayerStructureItemContainer.xaml.
+    /// </summary>
+    public partial class LayerStructureItemContainer : UserControl
+    {
+        public Layer Layer
+        {
+            get { return (Layer)GetValue(LayerProperty); }
+            set { SetValue(LayerProperty, value); }
+        }
+
+        // Using a DependencyProperty as the backing store for Layer.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty LayerProperty =
+            DependencyProperty.Register("Layer", typeof(Layer), typeof(LayerStructureItemContainer), new PropertyMetadata(default(Layer)));
+
+        public LayersViewModel LayerCommandsViewModel
+        {
+            get { return (LayersViewModel)GetValue(LayerCommandsViewModelProperty); }
+            set { SetValue(LayerCommandsViewModelProperty, value); }
+        }
+
+        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty LayerCommandsViewModelProperty =
+            DependencyProperty.Register("LayerCommandsViewModel", typeof(LayersViewModel), typeof(LayerStructureItemContainer), new PropertyMetadata(default(LayersViewModel)));
+
+        public int ContainerIndex
+        {
+            get { return (int)GetValue(ContainerIndexProperty); }
+            set { SetValue(ContainerIndexProperty, value); }
+        }
+
+        // Using a DependencyProperty as the backing store for ContainerIndex.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty ContainerIndexProperty =
+            DependencyProperty.Register("ContainerIndex", typeof(int), typeof(LayerStructureItemContainer), new PropertyMetadata(0));
+
+        public LayerStructureItemContainer()
+        {
+            InitializeComponent();
+        }
+    }
+}

+ 93 - 0
PixiEditor/Views/UserControls/LayersManager.xaml

@@ -0,0 +1,93 @@
+<UserControl x:Class="PixiEditor.Views.UserControls.LayersManager"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:ui="clr-namespace:PixiEditor.Helpers.UI"
+             xmlns:local="clr-namespace:PixiEditor.Views.UserControls"
+             xmlns:vws="clr-namespace:PixiEditor.Views" 
+             xmlns:layers="clr-namespace:PixiEditor.Models.Layers"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" 
+             mc:Ignorable="d"
+             d:DesignHeight="450" d:DesignWidth="250" x:Name="layersManager">
+    <UserControl.Resources>
+        <converters:IndexOfConverter x:Key="IndexOfConverter"/>
+    </UserControl.Resources>
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="37.5"/>
+            <RowDefinition Height="15"/>
+            <RowDefinition Height="1*"/>
+        </Grid.RowDefinitions>
+        <DockPanel Background="{StaticResource MainColor}" Grid.Row="0" HorizontalAlignment="Stretch">
+            <StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
+                <Button Command="{Binding LayerCommandsViewModel.NewLayerCommand, ElementName=layersManager}" 
+                        Height="24" Width="24" Cursor="Hand" ToolTip="New Layer"
+                        CommandParameter="{Binding Path=SelectedItem, ElementName=layersManager}"
+                                                HorizontalAlignment="Stretch" Margin="5"
+                                                Style="{StaticResource ToolButtonStyle}">
+                    <Button.Background>
+                        <ImageBrush ImageSource="/Images/Layer-add.png"/>
+                    </Button.Background>
+                </Button>
+                <Button Command="{Binding LayerCommandsViewModel.NewGroupCommand, ElementName=layersManager}" 
+                        CommandParameter="{Binding Path=SelectedItem, ElementName=layersManager}"
+                        Height="24" Width="24" ToolTip="New Group" Cursor="Hand"
+                                                HorizontalAlignment="Stretch" Margin="5"
+                                                Style="{StaticResource ToolButtonStyle}">
+                    <Button.Background>
+                        <ImageBrush ImageSource="/Images/Folder-add.png"/>
+                    </Button.Background>
+                </Button>
+                <Button Command="{Binding LayerCommandsViewModel.DeleteSelectedCommand, ElementName=layersManager}"
+                        CommandParameter="{Binding ElementName=layersManager, Path=SelectedItem}" Height="24" Width="24" ToolTip="Delete selected" Cursor="Hand"
+                                                HorizontalAlignment="Stretch" Margin="5"
+                                                Style="{StaticResource ToolButtonStyle}">
+                    <Button.Background>
+                        <ImageBrush ImageSource="/Images/Trash.png"/>
+                    </Button.Background>
+                </Button>
+            </StackPanel>
+            <StackPanel Orientation="Horizontal" DockPanel.Dock="Right" Margin="0,0,10,0" HorizontalAlignment="Right">
+                <Label Content="Opacity" Foreground="White" VerticalAlignment="Center"/>
+                <vws:NumberInput
+                        Min="0" Max="100"
+                        x:Name="numberInput"
+                        IsEnabled="{Binding Path=OpacityInputEnabled, ElementName=layersManager}" 
+                        Width="40" Height="20"
+                        VerticalAlignment="Center"
+                        LostFocus="NumberInput_LostFocus"/>
+                <Label Content="%" Foreground="White" VerticalAlignment="Center"/>
+            </StackPanel>
+        </DockPanel>
+        <Separator Grid.Row="1" Margin="0,-12, 0, 0" BorderBrush="{StaticResource DarkerAccentColor}" BorderThickness="2" />
+        <DockPanel LastChildFill="True" Grid.Row="2" Margin="0, -12, 0, 0">
+            <TreeView DockPanel.Dock="Top" Name="treeView" ItemsSource="{Binding LayerTreeRoot, ElementName=layersManager}"  SelectedItemChanged="TreeView_SelectedItemChanged">
+                <TreeView.ItemsPanel>
+                    <ItemsPanelTemplate>
+                        <ui:ReversedOrderStackPanel/>
+                    </ItemsPanelTemplate>
+                </TreeView.ItemsPanel>
+                <TreeView.Resources>
+                    <HierarchicalDataTemplate DataType="{x:Type layers:LayerGroup}" ItemsSource="{Binding Items}">
+                        <local:LayerGroupControl GroupName="{Binding Name}" MouseDown="SelectActiveItem"
+                                             IsVisibleUndoTriggerable="{Binding StructureData.IsVisible}" 
+                                             GroupOpacity="{Binding StructureData.Opacity}"
+                                             LayersViewModel="{Binding LayerCommandsViewModel, ElementName=layersManager}" 
+                                             GroupGuid="{Binding GroupGuid}" 
+                                             GroupData="{Binding StructureData}"
+                                             MouseMove="LayerGroup_MouseMove"/>
+                    </HierarchicalDataTemplate>
+                    <DataTemplate DataType="{x:Type layers:Layer}">
+                        <local:LayerStructureItemContainer    
+                            MouseDown="SelectActiveItem"
+                            MouseMove="LayerStructureItemContainer_MouseMove" 
+                            ContainerIndex="{Binding Converter={StaticResource IndexOfConverter}}"
+                            Layer="{Binding}" LayerCommandsViewModel="{Binding LayerCommandsViewModel, ElementName=layersManager}"/>
+                    </DataTemplate>
+                </TreeView.Resources>
+            </TreeView>
+            <Border Name="dropBorder" DragEnter="Grid_DragEnter" DragLeave="Grid_DragLeave" AllowDrop="True" Drop="Grid_Drop" Background="Transparent" BorderThickness="0, 5, 0, 0"></Border>
+        </DockPanel>
+    </Grid>
+</UserControl>

+ 284 - 0
PixiEditor/Views/UserControls/LayersManager.xaml.cs

@@ -0,0 +1,284 @@
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Undo;
+using PixiEditor.ViewModels.SubViewModels.Main;
+using System;
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PixiEditor.Views.UserControls
+{
+    /// <summary>
+    /// Interaction logic for LayersManager.xaml.
+    /// </summary>
+    public partial class LayersManager : UserControl
+    {
+        public object SelectedItem
+        {
+            get { return (object)GetValue(SelectedItemProperty); }
+            set { SetValue(SelectedItemProperty, value); }
+        }
+
+        public static readonly DependencyProperty SelectedItemProperty =
+            DependencyProperty.Register("SelectedItem", typeof(object), typeof(LayersManager), new PropertyMetadata(0));
+
+
+        public ObservableCollection<object> LayerTreeRoot
+        {
+            get { return (ObservableCollection<object>)GetValue(LayerTreeRootProperty); }
+            set { SetValue(LayerTreeRootProperty, value); }
+        }
+
+        public static readonly DependencyProperty LayerTreeRootProperty =
+            DependencyProperty.Register(
+                "LayerTreeRoot",
+                typeof(ObservableCollection<object>),
+                typeof(LayersManager),
+                new PropertyMetadata(default(ObservableCollection<object>)));
+        public LayersViewModel LayerCommandsViewModel
+        {
+            get { return (LayersViewModel)GetValue(LayerCommandsViewModelProperty); }
+            set { SetValue(LayerCommandsViewModelProperty, value); }
+        }
+
+        public static readonly DependencyProperty LayerCommandsViewModelProperty =
+            DependencyProperty.Register("LayerCommandsViewModel", typeof(LayersViewModel), typeof(LayersManager), new PropertyMetadata(default(LayersViewModel), ViewModelChanged));
+
+        public bool OpacityInputEnabled
+        {
+            get { return (bool)GetValue(OpacityInputEnabledProperty); }
+            set { SetValue(OpacityInputEnabledProperty, value); }
+        }
+
+        public static readonly DependencyProperty OpacityInputEnabledProperty =
+            DependencyProperty.Register("OpacityInputEnabled", typeof(bool), typeof(LayersManager), new PropertyMetadata(false));
+
+        public LayersManager()
+        {
+            InitializeComponent();
+        }
+
+        private static void ViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            if (e.NewValue is LayersViewModel vm)
+            {
+                LayersManager manager = (LayersManager)d;
+                vm.Owner.BitmapManager.AddPropertyChangedCallback(nameof(vm.Owner.BitmapManager.ActiveDocument), () =>
+                {
+                    var doc = vm.Owner.BitmapManager.ActiveDocument;
+                    if (doc != null)
+                    {
+                        if (doc.ActiveLayer != null)
+                        {
+                            manager.SetActiveLayerAsSelectedItem(doc);
+                        }
+                        doc.AddPropertyChangedCallback(nameof(doc.ActiveLayer), () =>
+                        {
+                            manager.SetActiveLayerAsSelectedItem(doc);
+                        });
+                    }
+                });
+            }
+        }
+
+        private void SetActiveLayerAsSelectedItem(Document doc)
+        {
+            SelectedItem = doc.ActiveLayer;
+            SetInputOpacity(SelectedItem);
+        }
+
+        private void SetInputOpacity(object item)
+        {
+            if (item is Layer layer)
+            {
+                numberInput.Value = layer.Opacity * 100f;
+            }
+            else if (item is LayerStructureItemContainer container)
+            {
+                numberInput.Value = container.Layer.Opacity * 100f;
+            }
+            else if (item is LayerGroup group)
+            {
+                numberInput.Value = group.StructureData.Opacity * 100f;
+            }
+            else if (item is LayerGroupControl groupControl)
+            {
+                numberInput.Value = groupControl.GroupData.Opacity * 100f;
+            }
+        }
+
+        private void LayerStructureItemContainer_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
+        {
+            if (sender is LayerStructureItemContainer container && e.LeftButton == System.Windows.Input.MouseButtonState.Pressed)
+            {
+                Dispatcher.InvokeAsync(() => DragDrop.DoDragDrop(container, container, DragDropEffects.Move));
+            }
+        }
+
+        private void HandleGroupOpacityChange(GuidStructureItem group, float value)
+        {
+            if (LayerCommandsViewModel.Owner?.BitmapManager?.ActiveDocument != null)
+            {
+                var doc = LayerCommandsViewModel.Owner.BitmapManager.ActiveDocument;
+
+                var processArgs = new object[] { group.GroupGuid, value };
+                var reverseProcessArgs = new object[] { group.GroupGuid, group.Opacity };
+
+                ChangeGroupOpacityProcess(processArgs);
+
+                LayerCommandsViewModel.Owner.BitmapManager.ActiveDocument.LayerStructure.ExpandParentGroups(group);
+
+                doc.UndoManager.AddUndoChange(
+                new Change(
+                    ChangeGroupOpacityProcess,
+                    reverseProcessArgs,
+                    ChangeGroupOpacityProcess,
+                    processArgs,
+                    $"Change {group.Name} opacity"), false);
+            }
+        }
+
+        private void ChangeGroupOpacityProcess(object[] processArgs)
+        {
+            if (processArgs.Length > 0 && processArgs[0] is Guid groupGuid && processArgs[1] is float opacity)
+            {
+                var structure = LayerCommandsViewModel.Owner.BitmapManager.ActiveDocument.LayerStructure;
+                var group = structure.GetGroupByGuid(groupGuid);
+                group.Opacity = opacity;
+                var layers = structure.GetGroupLayers(group);
+                layers.ForEach(x => x.Opacity = x.Opacity); // This might seems stupid, but it raises property changed, without setting any value. This is used to trigger converters that use group opacity
+                numberInput.Value = opacity * 100;
+            }
+        }
+
+        private void LayerGroup_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
+        {
+            if (sender is LayerGroupControl container && e.LeftButton == System.Windows.Input.MouseButtonState.Pressed)
+            {
+                Dispatcher.InvokeAsync(() => DragDrop.DoDragDrop(container, container, DragDropEffects.Move));
+            }
+        }
+
+        private void NumberInput_LostFocus(object sender, RoutedEventArgs e)
+        {
+            float val = numberInput.Value / 100f;
+
+            object item = SelectedItem;
+
+            if (item is Layer || item is LayerStructureItemContainer)
+            {
+
+                Layer layer = null;
+
+                if (item is Layer lr)
+                {
+                    layer = lr;
+                }
+                else if (item is LayerStructureItemContainer container)
+                {
+                    layer = container.Layer;
+                }
+
+                HandleLayerOpacityChange(val, layer);
+            }
+            else if (item is LayerGroup group)
+            {
+                HandleGroupOpacityChange(group.StructureData, val);
+            }
+            else if (item is LayerGroupControl groupControl)
+            {
+                HandleGroupOpacityChange(groupControl.GroupData, val);
+            }
+        }
+
+        private void HandleLayerOpacityChange(float val, Layer layer)
+        {
+            float oldOpacity = layer.Opacity;
+
+            var doc = LayerCommandsViewModel.Owner.BitmapManager.ActiveDocument;
+
+            doc.RaisePropertyChange(nameof(doc.LayerStructure));
+
+            layer.OpacityUndoTriggerable = val;
+
+            doc.LayerStructure.ExpandParentGroups(layer.LayerGuid);
+
+            doc.RaisePropertyChange(nameof(doc.LayerStructure));
+
+            UndoManager undoManager = doc.UndoManager;
+
+
+            undoManager.AddUndoChange(
+                new Change(
+                    UpdateNumberInputLayerOpacityProcess,
+                    new object[] { oldOpacity },
+                    UpdateNumberInputLayerOpacityProcess,
+                    new object[] { val }));
+            undoManager.SquashUndoChanges(2);
+        }
+
+        private void UpdateNumberInputLayerOpacityProcess(object[] args)
+        {
+            if (args.Length > 0 && args[0] is float opacity)
+            {
+                numberInput.Value = opacity * 100;
+            }
+        }
+
+        private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
+        {
+            SetInputOpacity(SelectedItem);
+        }
+
+        private void Grid_Drop(object sender, DragEventArgs e)
+        {
+            dropBorder.BorderBrush = Brushes.Transparent;
+
+            if (e.Data.GetDataPresent(LayerGroupControl.LayerContainerDataName))
+            {
+                HandleLayerDrop(e.Data);
+            }
+
+            if (e.Data.GetDataPresent(LayerGroupControl.LayerGroupControlDataName))
+            {
+                HandleGroupControlDrop(e.Data);
+            }
+        }
+
+        private void HandleLayerDrop(IDataObject data)
+        {
+            var doc = LayerCommandsViewModel.Owner.BitmapManager.ActiveDocument;
+            if (doc.Layers.Count == 0) return;
+
+            var layerContainer = (LayerStructureItemContainer)data.GetData(LayerGroupControl.LayerContainerDataName);
+            var refLayer = doc.Layers[0].LayerGuid;
+            doc.MoveLayerInStructure(layerContainer.Layer.LayerGuid, refLayer);
+            doc.LayerStructure.AssignParent(layerContainer.Layer.LayerGuid, null);
+        }
+
+        private void HandleGroupControlDrop(IDataObject data)
+        {
+            var doc = LayerCommandsViewModel.Owner.BitmapManager.ActiveDocument;
+            var groupContainer = (LayerGroupControl)data.GetData(LayerGroupControl.LayerGroupControlDataName);
+            doc.LayerStructure.MoveGroup(groupContainer.GroupGuid, 0);
+        }
+
+        private void Grid_DragEnter(object sender, DragEventArgs e)
+        {
+            ((Border)sender).BorderBrush = LayerItem.HighlightColor;
+        }
+
+        private void Grid_DragLeave(object sender, DragEventArgs e)
+        {
+            ((Border)sender).BorderBrush = Brushes.Transparent;
+        }
+
+        private void SelectActiveItem(object sender, System.Windows.Input.MouseButtonEventArgs e)
+        {
+            SelectedItem = sender;
+        }
+    }
+}

+ 72 - 0
PixiEditor/Views/UserControls/ListSwitchButton.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PixiEditor.Views.UserControls
+{
+    /// <summary>
+    /// Interaction logic for ListSwitchButton.xaml
+    /// </summary>
+    public class ListSwitchButton : Button
+    {
+        public ObservableCollection<SwitchItem> Items
+        {
+            get { return (ObservableCollection<SwitchItem>)GetValue(ItemsProperty); }
+            set { SetValue(ItemsProperty, value); }
+        }
+
+        // Using a DependencyProperty as the backing store for Items.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty ItemsProperty =
+            DependencyProperty.Register("Items", typeof(ObservableCollection<SwitchItem>), typeof(ListSwitchButton), new PropertyMetadata(default(ObservableCollection<SwitchItem>), CollChanged));
+
+        private static void CollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            ListSwitchButton button = (ListSwitchButton)d;
+
+            ObservableCollection<SwitchItem> oldVal = (ObservableCollection<SwitchItem>)e.OldValue;
+            ObservableCollection<SwitchItem> newVal = (ObservableCollection<SwitchItem>)e.NewValue;
+            if ((oldVal == null || oldVal.Count == 0) && newVal != null && newVal.Count > 0)
+            {
+                button.ActiveItem = newVal[0];
+            }
+        }
+
+        public SwitchItem ActiveItem
+        {
+            get { return (SwitchItem)GetValue(ActiveItemProperty); }
+            set { SetValue(ActiveItemProperty, value); }
+        }
+
+        // Using a DependencyProperty as the backing store for ActiveItem.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty ActiveItemProperty =
+            DependencyProperty.Register("ActiveItem", typeof(SwitchItem), typeof(ListSwitchButton), new PropertyMetadata(new SwitchItem(Brushes.Transparent, "", null)));
+
+
+        static ListSwitchButton()
+        {
+            DefaultStyleKeyProperty.OverrideMetadata(typeof(ListSwitchButton), new FrameworkPropertyMetadata(typeof(ListSwitchButton)));
+        }
+
+        public ListSwitchButton()
+        {
+            Click += ListSwitchButton_Click;
+        }
+
+        private void ListSwitchButton_Click(object sender, RoutedEventArgs e)
+        {
+            if (!Items.Contains(ActiveItem))
+            {
+                throw new ArgumentException("Items doesn't contain specified Item.");
+            }
+
+            int index = Items.IndexOf(ActiveItem) + 1;
+            if (index > Items.Count - 1)
+            {
+                index = 0;
+            }
+            ActiveItem = Items[Math.Clamp(index, 0, Items.Count - 1)];
+        }
+    }
+}

+ 61 - 88
PixiEditor/Views/UserControls/PreviewWindow.xaml

@@ -4,7 +4,8 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
              xmlns:local="clr-namespace:PixiEditor.Views.UserControls"
-             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             xmlns:coll="clr-namespace:System.Collections.ObjectModel;assembly=System"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters" xmlns:system="clr-namespace:System;assembly=System.Runtime"
              mc:Ignorable="d" 
              d:DesignHeight="400" d:DesignWidth="400" x:Name="uc"
              Foreground="White" Background="Transparent">
@@ -19,12 +20,12 @@
             <RowDefinition Height="5"/>
             <RowDefinition Height="Auto"/>
         </Grid.RowDefinitions>
-        
-        <Viewbox Margin="30" VerticalAlignment="Top">
-            <Grid x:Name="imageGrid"
+
+        <Viewbox Margin="30" VerticalAlignment="Center">
+            <Grid x:Name="imageGrid" RenderOptions.BitmapScalingMode="NearestNeighbor"
               Visibility="{Binding Document, Converter={StaticResource NullToVisibiltyConverter}, ElementName=uc}"
               Height="{Binding Document.Height, ElementName=uc}" Width="{Binding Document.Width, ElementName=uc}"
-              Background="{Binding SelectedItem.Tag, ElementName=backgroundComboBox}" d:Width="8" d:Height="8">
+              Background="{Binding ActiveItem.Value, ElementName=backgroundButton}" d:Width="8" d:Height="8">
                 <ItemsControl ItemsSource="{Binding Document.Layers, ElementName=uc}">
                     <ItemsControl.ItemsPanel>
                         <ItemsPanelTemplate>
@@ -34,10 +35,21 @@
                     <ItemsControl.ItemTemplate>
                         <DataTemplate>
                             <Image VerticalAlignment="Top" HorizontalAlignment="Left" Source="{Binding LayerBitmap}"
-                                               Visibility="{Binding IsVisible, Converter={StaticResource BoolToVisibilityConverter}}"
                                                RenderOptions.BitmapScalingMode="NearestNeighbor" Stretch="Uniform"
-                                               Opacity="{Binding Opacity}"
-                                               Width="{Binding Width}" Height="{Binding Height}" Margin="{Binding Offset}"  />
+                                               Width="{Binding Width}" Height="{Binding Height}" Margin="{Binding Offset}">
+                                <Image.Opacity>
+                                    <MultiBinding Converter="{converters:LayerToFinalOpacityConverter}">
+                                        <Binding Path="."/>
+                                        <Binding Path="Opacity"/>
+                                    </MultiBinding>
+                                </Image.Opacity>
+                                <Image.Visibility>
+                                    <MultiBinding Converter="{converters:FinalIsVisibleToVisiblityConverter}">
+                                        <Binding Path="."/>
+                                        <Binding Path="IsVisible"/>
+                                    </MultiBinding>
+                                </Image.Visibility>
+                            </Image>
                         </DataTemplate>
                     </ItemsControl.ItemTemplate>
                 </ItemsControl>
@@ -67,86 +79,47 @@
             <local:PrependTextBlock Prepend=" X: " Text="{Binding ColorCursorPosition.Left, ElementName=uc}"/>
             <local:PrependTextBlock Prepend=" Y: " Text="{Binding ColorCursorPosition.Top, ElementName=uc}"/>
 
-            <Grid Width="15"/>
-
-            <local:PrependTextBlock Prepend=" R: " Text="{Binding ColorCursorColor.R, ElementName=uc}"/>
-            <local:PrependTextBlock Prepend=" G: " Text="{Binding ColorCursorColor.G, ElementName=uc}"/>
-            <local:PrependTextBlock Prepend=" B: " Text="{Binding ColorCursorColor.B, ElementName=uc}"/>
-            <local:PrependTextBlock Prepend=" A: " Text="{Binding ColorCursorColor.A, ElementName=uc}"/>
-
-            <local:PrependTextBlock Prepend="  (" Text="{Binding ColorCursorColor, ElementName=uc, FallbackValue=#00000000}" Append=")"/>
+            <TextBlock VerticalAlignment="Center" Margin="10, 0, 0, 0">
+                <TextBlock.Text>
+                    <MultiBinding Converter="{converters:FormattedColorConverter}">
+                        <Binding Path="ColorCursorColor" ElementName="uc"/>
+                        <Binding Path="ActiveItem.Value" ElementName="formatButton"/>
+                    </MultiBinding>
+                </TextBlock.Text>
+            </TextBlock>
         </StackPanel>
-
-
-        <StackPanel Visibility="{Binding OptionsOpen, ElementName=uc, Converter={StaticResource BoolToVisibilityConverter}, FallbackValue=Hidden}"
-                    Background="{StaticResource AccentColor}" Grid.RowSpan="3">
-            <Grid Margin="5">
-                <Grid.ColumnDefinitions>
-                    <ColumnDefinition Width="Auto"/>
-                    <ColumnDefinition Width="*"/>
-                    <ColumnDefinition Width="50"/>
-                </Grid.ColumnDefinitions>
-                <TextBlock Text="Background: " VerticalAlignment="Center"/>
-                <ComboBox SelectedIndex="1" x:Name="backgroundComboBox" Grid.Column="1">
-                    <ComboBoxItem Content="None">
-                        <ComboBoxItem.Tag>
-                            <SolidColorBrush Color="Transparent"/>
-                        </ComboBoxItem.Tag>
-                    </ComboBoxItem>
-                    <ComboBoxItem Content="Checkered">
-                        <ComboBoxItem.Tag>
-                            <ImageBrush ImageSource="/Images/transparentbg.png"/>
-                        </ComboBoxItem.Tag>
-                    </ComboBoxItem>
-                    <ComboBoxItem Content="Black">
-                        <ComboBoxItem.Tag>
-                            <SolidColorBrush Color="Black"/>
-                        </ComboBoxItem.Tag>
-                    </ComboBoxItem>
-                    <ComboBoxItem Content="White">
-                        <ComboBoxItem.Tag>
-                            <SolidColorBrush Color="White"/>
-                        </ComboBoxItem.Tag>
-                    </ComboBoxItem>
-                </ComboBox>
-            </Grid>
-        </StackPanel>
-
-        <ToggleButton Height="28" VerticalAlignment="Top" HorizontalAlignment="Right"
-                 Margin="5" Background="Transparent" BorderThickness="0" IsChecked="{Binding OptionsOpen, ElementName=uc, Mode=TwoWay, FallbackValue=True}">
-            <ToggleButton.Style>
-                <Style TargetType="ToggleButton">
-                    <Setter Property="Template">
-                        <Setter.Value>
-                            <ControlTemplate TargetType="ToggleButton">
-                                <Border BorderBrush="{TemplateBinding BorderBrush}" 
-                                        Background="{TemplateBinding Background}">
-                                    <ContentPresenter HorizontalAlignment="Center"
-                                              VerticalAlignment="Center"/>
-                                </Border>
-                            </ControlTemplate>
-                        </Setter.Value>
-                    </Setter>
-                    <Style.Triggers>
-                        <Trigger Property="IsChecked" Value="False">
-                            <Setter Property="Foreground" Value="LightGray"/>
-                        </Trigger>
-                        <Trigger Property="IsChecked" Value="True">
-                            <Setter Property="Foreground" Value="White"/>
-                        </Trigger>
-                        <Trigger Property="IsMouseOver" Value="True">
-                            <Setter Property="Foreground" Value="Gray"/>
-                        </Trigger>
-                    </Style.Triggers>
-                </Style>
-            </ToggleButton.Style>
-            <Viewbox>
-                <Grid>
-                    <Path Stroke="{Binding Foreground, RelativeSource={RelativeSource FindAncestor, AncestorType=ToggleButton}, Mode=OneWay}" StrokeThickness="1.5" Data="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
-                    <Ellipse Stroke="{Binding Foreground, RelativeSource={RelativeSource FindAncestor, AncestorType=ToggleButton}, Mode=OneWay}" StrokeThickness="1.5" Height="8" Width="8" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
-                </Grid>
-            </Viewbox>
-        </ToggleButton>
-
+        <Grid Grid.Row="2" HorizontalAlignment="Right" Margin="0,0,5,0" RenderOptions.BitmapScalingMode="{Binding ElementName=backgroundButton, Path=ActiveItem.ScalingMode}">
+            <StackPanel Orientation="Horizontal">
+                <local:ListSwitchButton x:Name="formatButton" Margin="0,0,5,0" Height="20" Width="40" BorderBrush="Black">
+                    <local:ListSwitchButton.Items>
+                        <local:SwitchItemObservableCollection>
+                            <local:SwitchItem Content="RGBA" Background="#353535" Value="RGBA"/>
+                            <local:SwitchItem Content="HEX" Background="#353535" Value="HEX"/>
+                        </local:SwitchItemObservableCollection>
+                    </local:ListSwitchButton.Items>
+                </local:ListSwitchButton>
+                <local:ListSwitchButton BorderBrush="{StaticResource DarkerAccentColor}" Width="25" Height="20" x:Name="backgroundButton" ToolTip="Preview background">
+                    <local:ListSwitchButton.Items>
+                        <local:SwitchItemObservableCollection>
+                            <local:SwitchItem ScalingMode="NearestNeighbor">
+                                <local:SwitchItem.Background>
+                                    <ImageBrush ImageSource="/Images/CheckerTile.png" TileMode="Tile" Viewport="0, 0, 1, 1"/>
+                                </local:SwitchItem.Background>
+                                <local:SwitchItem.Value>
+                                    <ImageBrush Viewport="0, 0.05, 0.05, 0.05" ImageSource="/Images/CheckerTile.png" TileMode="Tile"/>
+                                </local:SwitchItem.Value>
+                            </local:SwitchItem>
+                            <local:SwitchItem Value="Transparent">
+                                <local:SwitchItem.Background>
+                                    <ImageBrush ImageSource="/Images/DiagonalRed.png"/>
+                                </local:SwitchItem.Background>
+                            </local:SwitchItem>
+                            <local:SwitchItem Background="White" Value="White"/>
+                            <local:SwitchItem Background="Black" Value="Black"/>
+                        </local:SwitchItemObservableCollection>
+                    </local:ListSwitchButton.Items>
+                </local:ListSwitchButton>
+            </StackPanel>
+        </Grid>
     </Grid>
 </UserControl>

+ 1 - 1
PixiEditor/Views/UserControls/PreviewWindow.xaml.cs

@@ -18,7 +18,7 @@ namespace PixiEditor.Views.UserControls
             DependencyProperty.Register(nameof(Document), typeof(Document), typeof(PreviewWindow));
 
         public Document Document
-        { 
+        {
             get => (Document)GetValue(DocumentProperty);
             set => SetValue(DocumentProperty, value);
         }

+ 76 - 0
PixiEditor/Views/UserControls/RawLayersViewer.xaml

@@ -0,0 +1,76 @@
+<UserControl x:Class="PixiEditor.Views.UserControls.RawLayersViewer"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:local="clr-namespace:PixiEditor.Views.UserControls" xmlns:ui="clr-namespace:PixiEditor.Helpers.UI" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             mc:Ignorable="d" 
+             d:DesignHeight="450" d:DesignWidth="250" Name="rawLayersControl">
+    <UserControl.Resources>
+        <converters:LayerStructureToGroupsConverter x:Key="LayerStructureToGroupsConverter"/>
+    </UserControl.Resources>
+    <StackPanel DataContext="{Binding ElementName=rawLayersControl}">
+        <ItemsControl ItemsSource="{Binding Layers}">
+            <ItemsControl.ItemsPanel>
+                <ItemsPanelTemplate>
+                    <ui:ReversedOrderStackPanel />
+                </ItemsPanelTemplate>
+            </ItemsControl.ItemsPanel>
+            <ItemsControl.ItemTemplate>
+                <DataTemplate>
+                    <StackPanel VerticalAlignment="Center" Orientation="Horizontal">
+                        <Label Foreground="White" Content="{Binding Name}"/>
+                        <TextBlock Foreground="Wheat" Text="{Binding LayerGuid}"/>
+                    </StackPanel>
+                </DataTemplate>
+            </ItemsControl.ItemTemplate>
+        </ItemsControl>
+        <Separator/>
+        <ItemsControl>
+            <ItemsControl.ItemsSource>
+                <MultiBinding Converter="{StaticResource LayerStructureToGroupsConverter}">
+                    <Binding Path="Structure"/>
+                    <Binding Path="Structure.Groups.Count"/>
+                    <Binding Path="Layers.Count"/>
+                </MultiBinding>
+            </ItemsControl.ItemsSource>
+            <ItemsControl.ItemTemplate>
+                <DataTemplate>
+                    <Border BorderThickness="1" BorderBrush="Black">
+                    <StackPanel>
+                            <Label Foreground="White" Content="{Binding Name}"/>
+                            <Label Foreground="White" Content="{Binding GroupGuid}"/>
+                            <StackPanel Orientation="Horizontal">
+                            <TextBlock Foreground="White" Text="Parent: "/>
+                                <TextBlock Foreground="Wheat" Text="{Binding Parent.Name}"/>
+                        </StackPanel>
+                        <StackPanel Orientation="Horizontal">
+                            <TextBlock Foreground="White" Text="End Layer: "/>
+                                <TextBlock Foreground="Wheat" Text="{Binding EndLayerGuid}"/>
+                        </StackPanel>
+                            <StackPanel Orientation="Horizontal">
+                                <TextBlock Foreground="White" Text="Start Layer: "/>
+                                <TextBlock Foreground="Wheat" Text="{Binding StartLayerGuid}"/>
+                            </StackPanel>
+                            <StackPanel Orientation="Horizontal">
+                                <TextBlock Foreground="White" Text="Subgroups: "/>
+                                <ItemsControl ItemsSource="{Binding Subgroups}">
+                                    <ItemsControl.ItemsPanel>
+                                        <ItemsPanelTemplate>
+                                            <StackPanel Orientation="Horizontal"/>
+                                        </ItemsPanelTemplate>
+                                    </ItemsControl.ItemsPanel>
+                                    <ItemsControl.ItemTemplate>
+                                        <DataTemplate>
+                                            <TextBlock Foreground="Wheat" Text="{Binding Name}"/>
+                                        </DataTemplate>
+                                    </ItemsControl.ItemTemplate>
+                                </ItemsControl>
+                            </StackPanel>
+                        </StackPanel>
+                    </Border>
+                </DataTemplate>
+            </ItemsControl.ItemTemplate>
+        </ItemsControl>
+    </StackPanel>
+</UserControl>

+ 44 - 0
PixiEditor/Views/UserControls/RawLayersViewer.xaml.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Controls;
+using PixiEditor.Models.Layers;
+
+namespace PixiEditor.Views.UserControls
+{
+    /// <summary>
+    /// Interaction logic for RawLayersViewer.xaml.
+    /// </summary>
+    public partial class RawLayersViewer : UserControl
+    {
+        public ObservableCollection<Layer> Layers
+        {
+            get { return (ObservableCollection<Layer>)GetValue(LayersProperty); }
+            set { SetValue(LayersProperty, value); }
+        }
+
+        // Using a DependencyProperty as the backing store for Layers.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty LayersProperty =
+            DependencyProperty.Register(
+                "Layers",
+                typeof(ObservableCollection<Layer>),
+                typeof(RawLayersViewer),
+                new PropertyMetadata(default(ObservableCollection<Layer>)));
+
+        public LayerStructure Structure
+        {
+            get { return (LayerStructure)GetValue(StructureProperty); }
+            set { SetValue(StructureProperty, value); }
+        }
+
+        // Using a DependencyProperty as the backing store for Structure.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty StructureProperty =
+            DependencyProperty.Register("Structure", typeof(LayerStructure), typeof(RawLayersViewer), new PropertyMetadata(default(LayerStructure)));
+
+        public RawLayersViewer()
+        {
+            InitializeComponent();
+        }
+    }
+}

+ 1 - 1
PixiEditor/Views/UserControls/SwatchesView.xaml

@@ -22,7 +22,7 @@
             <ItemsControl.ItemsPanel>
                 <ItemsPanelTemplate>
                     <WrapPanel Margin="10,10,0,10" Orientation="Horizontal"
-                               HorizontalAlignment="Center" VerticalAlignment="Top"/>
+                               HorizontalAlignment="Left" VerticalAlignment="Top"/>
                 </ItemsPanelTemplate>
             </ItemsControl.ItemsPanel>
             <ItemsControl.ItemTemplate>

+ 24 - 0
PixiEditor/Views/UserControls/SwitchItem.cs

@@ -0,0 +1,24 @@
+using System.Windows.Media;
+
+namespace PixiEditor.Views.UserControls
+{
+    public class SwitchItem
+    {
+        public string Content { get; set; } = "";
+        public Brush Background { get; set; }
+        public object Value { get; set; }
+
+        public BitmapScalingMode ScalingMode { get; set; } = BitmapScalingMode.HighQuality;
+
+        public SwitchItem(Brush background, object value, string content, BitmapScalingMode scalingMode = BitmapScalingMode.HighQuality)
+        {
+            Background = background;
+            Value = value;
+            ScalingMode = scalingMode;
+            Content = content;
+        }
+        public SwitchItem()
+        {
+        }
+    }
+}

+ 11 - 0
PixiEditor/Views/UserControls/SwitchItemObservableCollection.xaml.cs

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PixiEditor.Views.UserControls
+{
+
+    public class SwitchItemObservableCollection : ObservableCollection<SwitchItem> { }
+}

+ 3 - 2
PixiEditorTests/ModelsTests/ControllersTests/MockedSinglePixelPenTool.cs

@@ -1,4 +1,5 @@
-using System.Windows.Media;
+using System.Collections.Generic;
+using System.Windows.Media;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
@@ -8,7 +9,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
 {
     public class MockedSinglePixelPenTool : BitmapOperationTool
     {
-        public override LayerChange[] Use(Layer layer, Coordinates[] mouseMove, Color color)
+        public override LayerChange[] Use(Layer layer, List<Coordinates> mouseMove, Color color)
         {
             return Only(BitmapPixelChanges.FromSingleColoredArray(new[] { mouseMove[0] }, color), layer.LayerGuid);
         }

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

@@ -1,5 +1,6 @@
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Position;
+using System.Collections.Generic;
 using Xunit;
 
 namespace PixiEditorTests.ModelsTests.ControllersTests
@@ -12,7 +13,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
             bool toolUsed = false;
 
             ReadonlyToolUtility util = new ReadonlyToolUtility();
-            util.ExecuteTool(new[] { new Coordinates(0, 0) }, new TestReadonlyTool(() => toolUsed = true));
+            util.ExecuteTool(new List<Coordinates> { new Coordinates(0, 0) }, new TestReadonlyTool(() => toolUsed = true));
             Assert.True(toolUsed);
         }
     }

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

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 
@@ -13,7 +14,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
 
         public Action ToolAction { get; set; }
 
-        public override void Use(Coordinates[] pixels)
+        public override void Use(List<Coordinates> pixels)
         {
             ToolAction();
         }

+ 13 - 6
PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs

@@ -244,7 +244,7 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
         [InlineData(3, 1, 1)]
         [InlineData(3, 2, -2)]
         [InlineData(10, 9, -5)]
-        public void TestThatMoveLayerIndexByWorks(int layersAmount, int index, int amount)
+        public void TestThatMoveLayerInStructureWorks(int layersAmount, int index, int amount)
         {
             Document document = new Document(10, 10);
             for (int i = 0; i < layersAmount; i++)
@@ -253,20 +253,24 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
             }
 
             Guid oldGuid = document.Layers[index].LayerGuid;
-            document.MoveLayerIndexBy(index, amount);
+            Guid referenceGuid = document.Layers[index + amount].LayerGuid;
+            document.MoveLayerInStructure(oldGuid, referenceGuid, amount > 0);
 
             Assert.Equal(oldGuid, document.Layers[index + amount].LayerGuid);
         }
 
         [Fact]
-        public void TestThatMoveLayerIndexByUndoProcessWorks()
+        public void TestThatMoveLayerInStructureUndoProcessWorks()
         {
             Document document = new Document(10, 10);
 
             document.AddNewLayer("Test");
             document.AddNewLayer("Test2");
 
-            document.MoveLayerIndexBy(0, 1);
+            Guid oldGuid = document.Layers[0].LayerGuid;
+            Guid referenceGuid = document.Layers[1].LayerGuid;
+
+            document.MoveLayerInStructure(oldGuid, referenceGuid);
 
             document.UndoManager.Undo();
 
@@ -275,14 +279,17 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
         }
 
         [Fact]
-        public void TestThatMoveLayerIndexByRedoProcessWorks()
+        public void TestThatMoveLayerInStructureRedoProcessWorks()
         {
             Document document = new Document(10, 10);
 
             document.AddNewLayer("Test");
             document.AddNewLayer("Test2");
 
-            document.MoveLayerIndexBy(0, 1);
+            Guid oldGuid = document.Layers[0].LayerGuid;
+            Guid referenceGuid = document.Layers[1].LayerGuid;
+
+            document.MoveLayerInStructure(oldGuid, referenceGuid, true);
 
             document.UndoManager.Undo();
             document.UndoManager.Redo();

+ 243 - 0
PixiEditorTests/ModelsTests/DataHoldersTests/LayerStructureTests.cs

@@ -0,0 +1,243 @@
+using System;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.DataHoldersTests
+{
+    public class LayerStructureTests
+    {
+        [Fact]
+        public void TestThatAddNewGroupAddsNewGroup()
+        {
+            Document doc = new Document(1, 1);
+            doc.Layers.Add(new("_testLayer"));
+            var testLayer = doc.Layers[^1];
+            doc.LayerStructure.AddNewGroup("test", testLayer.LayerGuid);
+
+            Assert.Single(doc.LayerStructure.Groups);
+            Assert.Equal(testLayer.LayerGuid, doc.LayerStructure.Groups[0].StartLayerGuid);
+            Assert.Equal(testLayer.LayerGuid, doc.LayerStructure.Groups[0].EndLayerGuid);
+        }
+
+        [Fact]
+        public void TestThatAddNewGroupAddsNewGroupAsASubgroup()
+        {
+            Document doc = new Document(1, 1);
+            doc.Layers.Add(new("_testLayer"));
+            var testLayer = doc.Layers[^1];
+            doc.LayerStructure.AddNewGroup("test", testLayer.LayerGuid);
+            doc.LayerStructure.AddNewGroup("test1", testLayer.LayerGuid);
+
+            Assert.Single(doc.LayerStructure.Groups);
+            Assert.Single(doc.LayerStructure.Groups[0].Subgroups);
+            Assert.Equal(testLayer.LayerGuid, doc.LayerStructure.Groups[0].StartLayerGuid);
+            Assert.Equal(testLayer.LayerGuid, doc.LayerStructure.Groups[0].EndLayerGuid);
+            Assert.Equal(testLayer.LayerGuid, doc.LayerStructure.Groups[0].Subgroups[0].StartLayerGuid);
+            Assert.Equal(testLayer.LayerGuid, doc.LayerStructure.Groups[0].Subgroups[0].EndLayerGuid);
+        }
+
+        [Fact]
+        public void TestThatMoveGroupMovesSwapsLayerPlacesWithOtherGroup()
+        {
+            Document doc = new Document(1, 1);
+            doc.Layers.Add(new Layer("_testLayer"));
+            doc.Layers.Add(new Layer("_testLayer1"));
+            var testLayer = doc.Layers[0];
+            var testLayer1 = doc.Layers[^1];
+            doc.LayerStructure.AddNewGroup("test", testLayer.LayerGuid);
+            doc.LayerStructure.AddNewGroup("test1", testLayer1.LayerGuid);
+
+            Assert.Equal(0, doc.Layers.IndexOf(testLayer));
+            Assert.Equal(1, doc.Layers.IndexOf(testLayer1));
+
+            doc.LayerStructure.MoveGroup(doc.LayerStructure.Groups[0].GroupGuid, 1);
+
+            Assert.Equal(1, doc.Layers.IndexOf(testLayer));
+            Assert.Equal(0, doc.Layers.IndexOf(testLayer1));
+        }
+
+        [Fact]
+        public void TestThatIsChildOfDetectsNestedGroupCorrectly()
+        {
+            LayerStructure ls = new LayerStructure(new Document(0, 0));
+            Layer testLayer = new Layer("tst");
+            ls.Groups.Add(new GuidStructureItem("group 1", testLayer.LayerGuid));
+            ls.Groups[0].Subgroups.Add(new GuidStructureItem("group 1 nested", testLayer.LayerGuid));
+
+            Assert.True(ls.IsChildOf(ls.Groups[0].Subgroups[0], ls.Groups[0]));
+            Assert.False(ls.IsChildOf(ls.Groups[0], ls.Groups[0].Subgroups[0]));
+        }
+
+        [Fact]
+        public void TestThatIsChildOfDetectsNestedLayersCorrectly()
+        {
+            var doc = new Document(0, 0);
+            doc.Layers.Add(new Layer("tst"));
+            Guid testLayerGuid = doc.Layers[0].LayerGuid;
+            LayerStructure ls = new LayerStructure(doc);
+            ls.AddNewGroup("Test group", testLayerGuid);
+            ls.AddNewGroup("Test group nested", testLayerGuid);
+
+            Assert.True(ls.IsChildOf(testLayerGuid, ls.Groups[0]));
+            Assert.True(ls.IsChildOf(testLayerGuid, ls.Groups[0].Subgroups[0]));
+        }
+
+        [Fact]
+        public void TestThatGroupContainsOnlyLayerDetectsOnlySingleLayerCorrectly()
+        {
+            var doc = new Document(0, 0);
+            doc.Layers.Add(new Layer("layer"));
+            var guid = doc.Layers[0].LayerGuid;
+            doc.LayerStructure.AddNewGroup("layer group", guid);
+            Assert.True(LayerStructure.GroupContainsOnlyLayer(guid, doc.LayerStructure.Groups[0]));
+        }
+
+        [Fact]
+        public void TestThatGroupContainsOnlyLayerDetectsOnlySingleLayerThatIsNested()
+        {
+            var doc = new Document(0, 0);
+            doc.Layers.Add(new Layer("layer"));
+            var guid = doc.Layers[0].LayerGuid;
+            doc.LayerStructure.AddNewGroup("layer group", guid);
+            doc.LayerStructure.AddNewGroup("layer group nested", guid);
+            Assert.False(LayerStructure.GroupContainsOnlyLayer(guid, doc.LayerStructure.Groups[0]));
+            Assert.True(LayerStructure.GroupContainsOnlyLayer(guid, doc.LayerStructure.Groups[0].Subgroups[0]));
+        }
+
+        [Fact]
+        public void TestThatCloneReturnsSameLayerStructure()
+        {
+            Document doc = new(0, 0);
+            doc.Layers.Add(new("Test"));
+            doc.Layers.Add(new("Test2"));
+            LayerStructure structure = new(doc);
+            structure.AddNewGroup("Test group", doc.Layers[0].LayerGuid);
+
+            var clone = structure.CloneGroups();
+
+            Assert.Equal(structure.Groups.Count, clone.Count);
+            Assert.Single(clone);
+            Assert.Equal(structure.Groups[0].GroupGuid, clone[0].GroupGuid);
+        }
+
+        [Fact]
+        public void TestThatGetGroupByGuidReturnsNullForNonExistingGroup()
+        {
+            Document doc = new(0, 0);
+            doc.Layers.Add(new("Test"));
+
+            Assert.Null(doc.LayerStructure.GetGroupByGuid(null));
+            Assert.Null(doc.LayerStructure.GetGroupByGuid(Guid.NewGuid()));
+        }
+
+        [Fact]
+        public void TestThatGetGroupByGuidReturnsGroupCorrectly()
+        {
+            Document doc = new(0, 0);
+            doc.Layers.Add(new("Test"));
+            var group = doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].LayerGuid);
+
+            Assert.Equal(group.GroupGuid, doc.LayerStructure.GetGroupByGuid(group.GroupGuid).GroupGuid);
+        }
+
+        [Fact]
+        public void TestThatPreMoveReassignBoundsMakesNestedGroupEmptyAndRemovesItAndParent()
+        {
+            Document doc = new(0, 0);
+            doc.Layers.Add(new("Test"));
+            doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].LayerGuid);
+            var group1 = doc.LayerStructure.AddNewGroup("Test group nested", doc.Layers[0].LayerGuid);
+
+            doc.LayerStructure.PreMoveReassignBounds(new GroupData(group1.GroupGuid), doc.Layers[0].LayerGuid);
+
+            Assert.Empty(doc.LayerStructure.Groups);
+        }
+
+        [Fact]
+        public void TestThatPostMoveReassignBoundsAssignsNewLayerToGroup()
+        {
+            Document doc = new(0, 0);
+            doc.Layers.Add(new("Test"));
+            doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].LayerGuid);
+            var group1 = doc.LayerStructure.AddNewGroup("Test group nested", doc.Layers[0].LayerGuid);
+
+            doc.Layers.Add(new("Test 1"));
+
+            var firstLayer = doc.Layers[0];
+            var layer = doc.Layers[^1];
+
+            doc.LayerStructure.PostMoveReassignBounds(new GroupData(group1.GroupGuid), layer.LayerGuid);
+
+            Assert.Single(doc.LayerStructure.Groups);
+            Assert.Single(doc.LayerStructure.Groups[0].Subgroups);
+            Assert.Equal(layer.LayerGuid, doc.LayerStructure.Groups[0].Subgroups[0].EndLayerGuid);
+            Assert.Equal(firstLayer.LayerGuid, doc.LayerStructure.Groups[0].Subgroups[0].StartLayerGuid);
+            Assert.Equal(layer.LayerGuid, doc.LayerStructure.Groups[0].EndLayerGuid);
+            Assert.Equal(firstLayer.LayerGuid, doc.LayerStructure.Groups[0].StartLayerGuid);
+        }
+
+        [Fact]
+        public void TestThatAssignParentAssignsParent()
+        {
+            Document doc = new(0, 0);
+            doc.Layers.Add(new Layer("Test"));
+
+            var firstLayer = doc.Layers[0];
+
+            doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].LayerGuid);
+
+            doc.Layers.Add(new Layer("Test 1"));
+
+            var layer = doc.Layers[^1];
+
+            doc.LayerStructure.AssignParent(doc.Layers[^1].LayerGuid, doc.LayerStructure.Groups[0].GroupGuid);
+
+            Assert.Equal(layer.LayerGuid, doc.LayerStructure.Groups[0].EndLayerGuid);
+            Assert.Equal(firstLayer.LayerGuid, doc.LayerStructure.Groups[0].StartLayerGuid);
+        }
+
+        [Fact]
+        public void TestThatAssignParentDeAssignsParentOnNull()
+        {
+            Document doc = new(0, 0);
+            doc.Layers.Add(new Layer("Test"));
+
+            var firstLayer = doc.Layers[0];
+
+            doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].LayerGuid);
+
+            doc.Layers.Add(new Layer("Test 1"));
+
+            var layer = doc.Layers[^1];
+
+            doc.LayerStructure.AssignParent(layer.LayerGuid,  doc.LayerStructure.Groups[0].GroupGuid);
+            doc.LayerStructure.AssignParent(layer.LayerGuid, null);
+
+            Assert.Equal(firstLayer.LayerGuid, doc.LayerStructure.Groups[0].EndLayerGuid);
+            Assert.Equal(firstLayer.LayerGuid, doc.LayerStructure.Groups[0].StartLayerGuid);
+        }
+
+        [Fact]
+        public void TestThatGetGroupLayersReturnsAllLayersInGroup()
+        {
+            Document doc = new(0, 0);
+            doc.Layers.Add(new Layer("Test"));
+            doc.Layers.Add(new Layer("Test 1"));
+            doc.Layers.Add(new Layer("Test 2"));
+            doc.Layers.Add(new Layer("Test 3"));
+            doc.LayerStructure.AddNewGroup("Test group", doc.Layers[0].LayerGuid);
+
+            doc.LayerStructure.AssignParent(doc.Layers[1].LayerGuid, doc.LayerStructure.Groups[0].GroupGuid);
+            doc.LayerStructure.AssignParent(doc.Layers[2].LayerGuid, doc.LayerStructure.Groups[0].GroupGuid);
+            doc.LayerStructure.AddNewGroup("Test group", doc.Layers[2].LayerGuid);
+
+            var layersInGroup = doc.LayerStructure.GetGroupLayers(doc.LayerStructure.Groups[0]);
+
+            Assert.Equal(3, layersInGroup.Count);
+            Assert.Contains(doc.Layers[0], layersInGroup);
+            Assert.Contains(doc.Layers[1], layersInGroup);
+            Assert.Contains(doc.Layers[2], layersInGroup);
+        }
+    }
+}

+ 2 - 2
PixiEditorTests/PixiEditorTests.csproj

@@ -30,14 +30,14 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Codecov" Version="1.12.4" />
+    <PackageReference Include="Codecov" Version="1.13.0" />
     <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.2">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
     <PackageReference Include="Moq" Version="4.16.1" />
     <PackageReference Include="OpenCover" Version="4.7.922" />
     <PackageReference Include="xunit" Version="2.4.1" />