Browse Source

Merge pull request #164 from PixiEditor/cool-windows

Added Shortcut and Startup Popup
Krzysztof Krysiński 4 years ago
parent
commit
cbbe2788cd
38 changed files with 1192 additions and 381 deletions
  1. 44 0
      PixiEditor/Helpers/Converters/FileExtensionToImageSourceConverter.cs
  2. 35 0
      PixiEditor/Helpers/Converters/KeyToStringConverter.cs
  3. 17 0
      PixiEditor/Helpers/Extensions/EnumHelpers.cs
  4. 82 0
      PixiEditor/Helpers/InputKeyHelpers.cs
  5. BIN
      PixiEditor/Images/JpgFile.png
  6. BIN
      PixiEditor/Images/PixiFile.png
  7. BIN
      PixiEditor/Images/PixiParserLogo.png
  8. BIN
      PixiEditor/Images/PngFile.png
  9. BIN
      PixiEditor/Images/UnknownFile.png
  10. 16 1
      PixiEditor/Models/Controllers/Shortcuts/Shortcut.cs
  11. 5 4
      PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs
  12. 43 0
      PixiEditor/Models/Controllers/Shortcuts/ShortcutGroup.cs
  13. 4 3
      PixiEditor/Models/DataHolders/Document/Document.IO.cs
  14. 59 0
      PixiEditor/Models/DataHolders/RecentlyOpenedCollection.cs
  15. 108 0
      PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
  16. 0 60
      PixiEditor/Models/DataHolders/SerializableDocument.cs
  17. 66 14
      PixiEditor/Models/ImageManipulation/BitmapUtils.cs
  18. 0 77
      PixiEditor/Models/Layers/SerializableLayer.cs
  19. 21 7
      PixiEditor/Models/UserPreferences/PreferencesSettings.cs
  20. 10 0
      PixiEditor/PixiEditor.csproj
  21. 36 1
      PixiEditor/Styles/Titlebar.xaml
  22. 67 5
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  23. 20 0
      PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs
  24. 6 15
      PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs
  25. 7 0
      PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsGroup.cs
  26. 63 53
      PixiEditor/ViewModels/ViewModelMain.cs
  27. 161 0
      PixiEditor/Views/Dialogs/HelloTherePopup.xaml
  28. 115 0
      PixiEditor/Views/Dialogs/HelloTherePopup.xaml.cs
  29. 4 4
      PixiEditor/Views/Dialogs/SettingsWindow.xaml
  30. 123 0
      PixiEditor/Views/Dialogs/ShortcutPopup.xaml
  31. 63 0
      PixiEditor/Views/Dialogs/ShortcutPopup.xaml.cs
  32. 11 5
      PixiEditor/Views/MainWindow.xaml
  33. 1 1
      PixiEditor/Views/MainWindow.xaml.cs
  34. 3 3
      PixiEditorTests/ModelsTests/ControllersTests/ShortcutControllerTests.cs
  35. 1 1
      PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs
  36. 0 83
      PixiEditorTests/ModelsTests/DataHoldersTests/SerializableDocumentTests.cs
  37. 0 43
      PixiEditorTests/ModelsTests/IO/BinarySerializationTests.cs
  38. 1 1
      PixiEditorTests/ViewModelsTests/ViewModelMainTests.cs

+ 44 - 0
PixiEditor/Helpers/Converters/FileExtensionToImageSourceConverter.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Data;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class FileExtensionToImageSourceConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            string extension = (string)value;
+
+            if (extension == ".pixi")
+            {
+                return Join("PixiFile.png", parameter);
+            }
+            else if (extension == ".png")
+            {
+                return Join("PngFile.png", parameter);
+            }
+            else if (extension == ".jpg" || extension == ".jpeg")
+            {
+                return Join("JpgFile.png", parameter);
+            }
+
+            return Join("UnknownFile.png", parameter);
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+
+        private string Join(string path, object parameter)
+        {
+            return Path.Join((string)parameter, "Images", path);
+        }
+    }
+}

+ 35 - 0
PixiEditor/Helpers/Converters/KeyToStringConverter.cs

@@ -0,0 +1,35 @@
+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.Input;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class KeyToStringConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value is Key key)
+            {
+                return InputKeyHelpers.GetCharFromKey(key);
+            }
+            else if (value is ModifierKeys)
+            {
+                return value.ToString();
+            }
+            else
+            {
+                return string.Empty;
+            }
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 17 - 0
PixiEditor/Helpers/Extensions/EnumHelpers.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    public static class EnumHelpers
+    {
+        public static IEnumerable<T> GetFlags<T>(this T e)
+               where T : Enum
+        {
+            return Enum.GetValues(e.GetType()).Cast<T>().Where(x => e.HasFlag(x));
+        }
+    }
+}

+ 82 - 0
PixiEditor/Helpers/InputKeyHelpers.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Input;
+
+namespace PixiEditor.Helpers
+{
+    public static class InputKeyHelpers
+    {
+        public static string GetCharFromKey(Key key)
+        {
+            int virtualKey = KeyInterop.VirtualKeyFromKey(key);
+            byte[] keyboardState = new byte[256];
+            GetKeyboardState(keyboardState);
+
+            uint scanCode = MapVirtualKeyW((uint)virtualKey, MapType.MAPVK_VK_TO_VSC);
+            StringBuilder stringBuilder = new (3);
+
+            int result = ToUnicode((uint)virtualKey, scanCode, keyboardState, stringBuilder, stringBuilder.Capacity, 0);
+
+            switch (result)
+            {
+                case 0:
+                    {
+                        return key.ToString();
+                    }
+
+                case -1:
+                    {
+                        return stringBuilder.ToString().ToUpper();
+                    }
+
+                default:
+                    {
+                        return stringBuilder[result - 1].ToString().ToUpper();
+                    }
+            }
+        }
+
+        private enum MapType : uint
+        {
+            /// <summary>
+            /// The uCode parameter is a virtual-key code and is translated into a scan code. If it is a virtual-key code that does not distinguish between left- and right-hand keys, the left-hand scan code is returned. If there is no translation, the function returns 0.
+            /// </summary>
+            MAPVK_VK_TO_VSC = 0x0,
+
+            /// <summary>
+            /// The uCode parameter is a scan code and is translated into a virtual-key code that does not distinguish between left- and right-hand keys. If there is no translation, the function returns 0.
+            /// </summary>
+            MAPVK_VSC_TO_VK = 0x1,
+
+            /// <summary>
+            /// The uCode parameter is a virtual-key code and is translated into an unshifted character value in the low order word of the return value. Dead keys (diacritics) are indicated by setting the top bit of the return value. If there is no translation, the function returns 0.
+            /// </summary>
+            MAPVK_VK_TO_CHAR = 0x2,
+
+            /// <summary>
+            /// The uCode parameter is a scan code and is translated into a virtual-key code that distinguishes between left- and right-hand keys. If there is no translation, the function returns 0.
+            /// </summary>
+            MAPVK_VSC_TO_VK_EX = 0x3,
+        }
+
+        [DllImport("user32.dll")]
+        private static extern int ToUnicode(
+            uint wVirtKey,
+            uint wScanCode,
+            byte[] lpKeyState,
+            [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
+            StringBuilder pwszBuff,
+            int cchBuff,
+            uint wFlags);
+
+        [DllImport("user32.dll")]
+        private static extern bool GetKeyboardState(byte[] lpKeyState);
+
+        [DllImport("user32.dll")]
+        private static extern uint MapVirtualKeyW(uint uCode, MapType uMapType);
+    }
+}

BIN
PixiEditor/Images/JpgFile.png


BIN
PixiEditor/Images/PixiFile.png


BIN
PixiEditor/Images/PixiParserLogo.png


BIN
PixiEditor/Images/PngFile.png


BIN
PixiEditor/Images/UnknownFile.png


+ 16 - 1
PixiEditor/Models/Controllers/Shortcuts/Shortcut.cs

@@ -1,4 +1,6 @@
-using System.Windows.Input;
+using System.Linq;
+using System.Windows.Input;
+using PixiEditor.Helpers.Extensions;
 
 namespace PixiEditor.Models.Controllers.Shortcuts
 {
@@ -12,12 +14,25 @@ namespace PixiEditor.Models.Controllers.Shortcuts
             CommandParameter = commandParameter;
         }
 
+        public Shortcut(Key shortcutKey, ICommand command, string description, object commandParameter = null, ModifierKeys modifier = ModifierKeys.None)
+            : this(shortcutKey, command, commandParameter, modifier)
+        {
+            Description = description;
+        }
+
         public Key ShortcutKey { get; set; }
 
         public ModifierKeys Modifier { get; set; }
 
+        /// <summary>
+        /// Gets all <see cref="ModifierKeys"/> as an array.
+        /// </summary>
+        public ModifierKeys[] Modifiers { get => Modifier.GetFlags().Except(new ModifierKeys[] { ModifierKeys.None }).ToArray(); }
+
         public ICommand Command { get; set; }
 
+        public string Description { get; set; }
+
         public object CommandParameter { get; set; }
 
         public void Execute()

+ 5 - 4
PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
 using System.Windows.Input;
 
@@ -6,14 +7,14 @@ namespace PixiEditor.Models.Controllers.Shortcuts
 {
     public class ShortcutController
     {
-        public ShortcutController()
+        public ShortcutController(params ShortcutGroup[] shortcutGroups)
         {
-            Shortcuts = new List<Shortcut>();
+            ShortcutGroups = new ObservableCollection<ShortcutGroup>(shortcutGroups);
         }
 
         public static bool BlockShortcutExecution { get; set; }
 
-        public List<Shortcut> Shortcuts { get; set; }
+        public ObservableCollection<ShortcutGroup> ShortcutGroups { get; init; }
 
         public Shortcut LastShortcut { get; private set; }
 
@@ -21,7 +22,7 @@ namespace PixiEditor.Models.Controllers.Shortcuts
         {
             if (!BlockShortcutExecution)
             {
-                Shortcut[] shortcuts = Shortcuts.FindAll(x => x.ShortcutKey == key).ToArray();
+                Shortcut[] shortcuts = ShortcutGroups.SelectMany(x => x.Shortcuts).ToList().FindAll(x => x.ShortcutKey == key).ToArray();
                 if (shortcuts.Length < 1)
                 {
                     return;

+ 43 - 0
PixiEditor/Models/Controllers/Shortcuts/ShortcutGroup.cs

@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.Controllers.Shortcuts
+{
+    public class ShortcutGroup
+    {
+        /// <summary>
+        /// Gets or sets the shortcuts in the shortcuts group.
+        /// </summary>
+        public ObservableCollection<Shortcut> Shortcuts { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name of the shortcut group.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether gets or sets the shortcut group visible in the shortcut popup.
+        /// </summary>
+        public bool IsVisible { get; set; }
+
+        public ShortcutGroup(string name, params Shortcut[] shortcuts)
+        {
+            Name = name;
+            Shortcuts = new ObservableCollection<Shortcut>(shortcuts);
+            IsVisible = true;
+        }
+
+        /// <param name="name">The name of the group.</param>
+        /// <param name="shortcuts">The shortcuts that belong in the group.</param>
+        /// <param name="isVisible">Is the group visible in the shortcut popup.</param>
+        public ShortcutGroup(string name, bool isVisible = true, params Shortcut[] shortcuts)
+            : this(name, shortcuts)
+        {
+            IsVisible = isVisible;
+        }
+    }
+}

+ 4 - 3
PixiEditor/Models/DataHolders/Document/Document.IO.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using System.Linq;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserPreferences;
 
@@ -53,7 +54,7 @@ namespace PixiEditor.Models.DataHolders
 
         private void UpdateRecentlyOpened(string newPath)
         {
-            ObservableCollection<string> recentlyOpened = XamlAccesibleViewModel.FileSubViewModel.RecentlyOpened;
+            RecentlyOpenedCollection recentlyOpened = XamlAccesibleViewModel.FileSubViewModel.RecentlyOpened;
 
             if (!recentlyOpened.Contains(newPath))
             {
@@ -65,7 +66,7 @@ namespace PixiEditor.Models.DataHolders
                 recentlyOpened.Move(index, 0);
             }
 
-            if (recentlyOpened.Count > IPreferences.Current.GetPreference("maxOpenedRecently", 10))
+            if (recentlyOpened.Count > IPreferences.Current.GetPreference("MaxOpenedRecently", 8))
             {
                 for (int i = 4; i < recentlyOpened.Count; i++)
                 {
@@ -73,7 +74,7 @@ namespace PixiEditor.Models.DataHolders
                 }
             }
 
-            IPreferences.Current.UpdateLocalPreference("RecentlyOpened", recentlyOpened);
+            IPreferences.Current.UpdateLocalPreference("RecentlyOpened", recentlyOpened.Select(x => x.FilePath));
 
             XamlAccesibleViewModel.FileSubViewModel.HasRecent = true;
         }

+ 59 - 0
PixiEditor/Models/DataHolders/RecentlyOpenedCollection.cs

@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public class RecentlyOpenedCollection : ObservableCollection<RecentlyOpenedDocument>
+    {
+        public RecentlyOpenedDocument this[string path]
+        {
+            get
+            {
+                return Get(path);
+            }
+        }
+
+        public RecentlyOpenedCollection()
+        {
+        }
+
+        public RecentlyOpenedCollection(IEnumerable<RecentlyOpenedDocument> documents)
+            : base(documents)
+        {
+        }
+
+        public void Add(string path)
+        {
+            if (string.IsNullOrWhiteSpace(path))
+            {
+                return;
+            }
+
+            Add(Create(path));
+        }
+
+        public bool Contains(string path) => Get(path) is not null;
+
+        public void Remove(string path) => Remove(Get(path));
+
+        public int IndexOf(string path) => IndexOf(Get(path));
+
+        public void Insert(int index, string path)
+        {
+            if (string.IsNullOrWhiteSpace(path))
+            {
+                return;
+            }
+
+            Insert(index, Create(path));
+        }
+
+        private static RecentlyOpenedDocument Create(string path) => new (path);
+
+        private RecentlyOpenedDocument Get(string path) => this.FirstOrDefault(x => x.FilePath == path);
+    }
+}

+ 108 - 0
PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs

@@ -0,0 +1,108 @@
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Windows.Media.Imaging;
+using PixiEditor.Helpers;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.Layers;
+using PixiEditor.Parser;
+
+namespace PixiEditor.Models.DataHolders
+{
+    [DebuggerDisplay("{FilePath}")]
+    public class RecentlyOpenedDocument : NotifyableObject
+    {
+        private bool corrupt;
+
+        private string filePath;
+
+        private WriteableBitmap previewBitmap;
+
+        public string FilePath
+        {
+            get => filePath;
+            set
+            {
+                SetProperty(ref filePath, value);
+                RaisePropertyChanged(nameof(FileName));
+                RaisePropertyChanged(nameof(FileExtension));
+                PreviewBitmap = null;
+            }
+        }
+
+        public bool Corrupt { get => corrupt; set => SetProperty(ref corrupt, value); }
+
+        public string FileName => Path.GetFileNameWithoutExtension(filePath);
+
+        public string FileExtension
+        {
+            get
+            {
+                if (Corrupt)
+                {
+                    return "Corrupt";
+                }
+
+                return Path.GetExtension(filePath).ToLower();
+            }
+        }
+
+        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+        public WriteableBitmap PreviewBitmap
+        {
+            get
+            {
+                if (previewBitmap == null)
+                {
+                    PreviewBitmap = LoadPreviewBitmap();
+                }
+
+                return previewBitmap;
+            }
+            private set => SetProperty(ref previewBitmap, value);
+        }
+
+        public RecentlyOpenedDocument(string path)
+        {
+            FilePath = path;
+        }
+
+        private WriteableBitmap LoadPreviewBitmap()
+        {
+            if (FileExtension == ".pixi")
+            {
+                SerializableDocument serializableDocument = null;
+
+                try
+                {
+                    serializableDocument = PixiParser.Deserialize(filePath);
+                }
+                catch
+                {
+                    corrupt = true;
+                    return null;
+                }
+
+                return BitmapUtils.GeneratePreviewBitmap(serializableDocument.Layers, serializableDocument.Width, serializableDocument.Height, 80, 50);
+            }
+            else if (FileExtension == ".png" || FileExtension == ".jpg" || FileExtension == ".jpeg")
+            {
+                WriteableBitmap bitmap = null;
+
+                try
+                {
+                    bitmap = Importer.ImportImage(FilePath);
+                }
+                catch
+                {
+                    corrupt = true;
+                }
+
+                return bitmap;
+            }
+
+            return null;
+        }
+    }
+}

+ 0 - 60
PixiEditor/Models/DataHolders/SerializableDocument.cs

@@ -1,60 +0,0 @@
-using System;
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Windows;
-using System.Windows.Media;
-using PixiEditor.Models.ImageManipulation;
-using PixiEditor.Models.Layers;
-
-namespace PixiEditor.Models.DataHolders
-{
-    [Serializable]
-    public class SerializableDocument
-    {
-        public SerializableDocument(Document document)
-        {
-            Width = document.Width;
-            Height = document.Height;
-            Layers = document.Layers.Select(x => new SerializableLayer(x)).ToArray();
-            Swatches = document.Swatches.Select(x => new Tuple<byte, byte, byte, byte>(x.A, x.R, x.G, x.B)).ToArray();
-        }
-
-        public int Width { get; set; }
-
-        public int Height { get; set; }
-
-        public SerializableLayer[] Layers { get; set; }
-
-        public Tuple<byte, byte, byte, byte>[] Swatches { get; set; }
-
-        public Document ToDocument()
-        {
-            Document document = new Document(Width, Height)
-            {
-                Layers = ToLayers(),
-                Swatches = new ObservableCollection<Color>(Swatches.Select(x =>
-                    Color.FromArgb(x.Item1, x.Item2, x.Item3, x.Item4)))
-            };
-            return document;
-        }
-
-        public ObservableCollection<Layer> ToLayers()
-        {
-            ObservableCollection<Layer> layers = new ObservableCollection<Layer>();
-            for (int i = 0; i < Layers.Length; i++)
-            {
-                SerializableLayer serLayer = Layers[i];
-                Layer layer =
-                    new Layer(serLayer.Name, BitmapUtils.BytesToWriteableBitmap(serLayer.Width, serLayer.Height, serLayer.BitmapBytes))
-                    {
-                        IsVisible = serLayer.IsVisible,
-                        Offset = new Thickness(serLayer.OffsetX, serLayer.OffsetY, 0, 0),
-                        Opacity = serLayer.Opacity
-                    };
-                layers.Add(layer);
-            }
-
-            return layers;
-        }
-    }
-}

+ 66 - 14
PixiEditor/Models/ImageManipulation/BitmapUtils.cs

@@ -7,6 +7,7 @@ using System.Windows.Media.Imaging;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
+using PixiEditor.Parser;
 
 namespace PixiEditor.Models.ImageManipulation
 {
@@ -89,21 +90,30 @@ namespace PixiEditor.Models.ImageManipulation
         /// <returns>WriteableBitmap image.</returns>
         public static WriteableBitmap GeneratePreviewBitmap(Document document, int maxPreviewWidth, int maxPreviewHeight)
         {
-            WriteableBitmap previewBitmap = BitmapFactory.New(document.Width, document.Height);
-
-            // 0.8 because blit doesn't take into consideration layer opacity. Small images with opacity > 80% are simillar enough.
-            foreach (var layer in document.Layers.Where(x => x.IsVisible && x.Opacity > 0.8f))
-            {
-                previewBitmap.Blit(
-                    new Rect(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height),
-                    layer.LayerBitmap,
-                    new Rect(0, 0, layer.Width, layer.Height));
-            }
-
-            int width = document.Width >= document.Height ? maxPreviewWidth : (int)Math.Ceiling(document.Width / ((float)document.Height / maxPreviewHeight));
-            int height = document.Height > document.Width ? maxPreviewHeight : (int)Math.Ceiling(document.Height / ((float)document.Width / maxPreviewWidth));
+            var opacityLayers = document.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),
+                document.Width,
+                document.Height,
+                maxPreviewWidth,
+                maxPreviewHeight);
+        }
 
-            return previewBitmap.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+        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);
+
+            return GeneratePreviewBitmap(
+                opacityLayers.Select(x => BytesToWriteableBitmap(x.Width, x.Height, x.BitmapBytes)),
+                opacityLayers.Select(x => x.OffsetX),
+                opacityLayers.Select(x => x.OffsetY),
+                width,
+                height,
+                maxPreviewWidth,
+                maxPreviewHeight);
         }
 
         public static Dictionary<Guid, Color[]> GetPixelsForSelection(Layer[] layers, Coordinates[] selection)
@@ -134,5 +144,47 @@ namespace PixiEditor.Models.ImageManipulation
 
             return result;
         }
+
+        private static WriteableBitmap GeneratePreviewBitmap(
+            IEnumerable<WriteableBitmap> layerBitmaps,
+            IEnumerable<int> offsetsX,
+            IEnumerable<int> offsetsY,
+            int width,
+            int height,
+            int maxPreviewWidth,
+            int maxPreviewHeight)
+        {
+            int count = layerBitmaps.Count();
+
+            if (count != offsetsX.Count() || count != offsetsY.Count())
+            {
+                throw new ArgumentException("There were not the same amount of bitmaps and offsets", nameof(layerBitmaps));
+            }
+
+            WriteableBitmap previewBitmap = BitmapFactory.New(width, height);
+
+            var layerBitmapsEnumerator = layerBitmaps.GetEnumerator();
+            var offsetsXEnumerator = offsetsX.GetEnumerator();
+            var offsetsYEnumerator = offsetsY.GetEnumerator();
+
+            while (layerBitmapsEnumerator.MoveNext())
+            {
+                offsetsXEnumerator.MoveNext();
+                offsetsYEnumerator.MoveNext();
+
+                var bitmap = layerBitmapsEnumerator.Current;
+                var offsetX = offsetsXEnumerator.Current;
+                var offsetY = offsetsYEnumerator.Current;
+
+                previewBitmap.Blit(
+                    new Rect(offsetX, offsetY, bitmap.Width, bitmap.Height),
+                    bitmap,
+                    new Rect(0, 0, bitmap.Width, bitmap.Height));
+            }
+
+            int newWidth = width >= height ? maxPreviewWidth : (int)Math.Ceiling(width / ((float)height / maxPreviewHeight));
+            int newHeight = height > width ? maxPreviewHeight : (int)Math.Ceiling(height / ((float)width / maxPreviewWidth));
+            return previewBitmap.Resize(newWidth, newHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+        }
     }
 }

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

@@ -1,77 +0,0 @@
-using System;
-using System.Linq;
-
-namespace PixiEditor.Models.Layers
-{
-    [Serializable]
-    public class SerializableLayer
-    {
-        public SerializableLayer(Layer layer)
-        {
-            Name = layer.Name;
-            Width = layer.Width;
-            Height = layer.Height;
-            BitmapBytes = layer.ConvertBitmapToBytes();
-            IsVisible = layer.IsVisible;
-            OffsetX = (int)layer.Offset.Left;
-            OffsetY = (int)layer.Offset.Top;
-            Opacity = layer.Opacity;
-            MaxWidth = layer.MaxWidth;
-            MaxHeight = layer.MaxHeight;
-        }
-
-        public string Name { get; set; }
-
-        public int Width { get; set; }
-
-        public int Height { get; set; }
-
-        public int MaxWidth { get; set; }
-
-        public int MaxHeight { get; set; }
-
-        public byte[] BitmapBytes { get; set; }
-
-        public bool IsVisible { get; set; }
-
-        public int OffsetX { get; set; }
-
-        public int OffsetY { get; set; }
-
-        public float Opacity { get; set; }
-
-        public override bool Equals(object obj)
-        {
-            if (obj == null || obj.GetType() != typeof(SerializableLayer))
-            {
-                return false;
-            }
-
-            SerializableLayer layer = (SerializableLayer)obj;
-
-            return Equals(layer);
-        }
-
-        public override int GetHashCode()
-        {
-            HashCode hashCode = default(HashCode);
-            hashCode.Add(Name);
-            hashCode.Add(Width);
-            hashCode.Add(Height);
-            hashCode.Add(MaxWidth);
-            hashCode.Add(MaxHeight);
-            hashCode.Add(BitmapBytes);
-            hashCode.Add(IsVisible);
-            hashCode.Add(OffsetX);
-            hashCode.Add(OffsetY);
-            hashCode.Add(Opacity);
-            return hashCode.ToHashCode();
-        }
-
-        protected bool Equals(SerializableLayer other)
-        {
-            return Name == other.Name && Width == other.Width && Height == other.Height && MaxWidth == other.MaxWidth && MaxHeight == other.MaxHeight &&
-                   BitmapBytes.SequenceEqual(other.BitmapBytes) && IsVisible == other.IsVisible && OffsetX == other.OffsetX && OffsetY == other.OffsetY && Opacity.Equals(other.Opacity);
-        }
-    }
-}

+ 21 - 7
PixiEditor/Models/UserPreferences/PreferencesSettings.cs

@@ -119,14 +119,21 @@ namespace PixiEditor.Models.UserPreferences
                 Init();
             }
 
-            return Preferences.ContainsKey(name)
-                ? (T)Preferences[name]
-                : fallbackValue;
+            try
+            {
+                return Preferences.ContainsKey(name)
+                        ? (T)Convert.ChangeType(Preferences[name], typeof(T))
+                        : fallbackValue;
+            }
+            catch (InvalidCastException)
+            {
+                return fallbackValue;
+            }
         }
 
         public T? GetLocalPreference<T>(string name)
         {
-            return GetPreference(name, default(T));
+            return GetLocalPreference(name, default(T));
         }
 
         public T? GetLocalPreference<T>(string name, T? fallbackValue)
@@ -136,9 +143,16 @@ namespace PixiEditor.Models.UserPreferences
                 Init();
             }
 
-            return LocalPreferences.ContainsKey(name)
-                ? (T)LocalPreferences[name]
-                : fallbackValue;
+            try
+            {
+                return LocalPreferences.ContainsKey(name)
+                    ? (T)Convert.ChangeType(LocalPreferences[name], typeof(T))
+                    : fallbackValue;
+            }
+            catch (InvalidCastException)
+            {
+                return fallbackValue;
+            }
         }
 
 #nullable disable

+ 10 - 0
PixiEditor/PixiEditor.csproj

@@ -37,11 +37,16 @@
     <None Remove="Images\AnchorDot.png" />
     <None Remove="Images\Eye-off.png" />
     <None Remove="Images\Eye.png" />
+    <None Remove="Images\JpgFile.png" />
     <None Remove="Images\MoveImage.png" />
     <None Remove="Images\MoveViewportImage.png" />
     <None Remove="Images\PixiBotLogo.png" />
     <None Remove="Images\PixiEditorLogo.png" />
+    <None Remove="Images\PixiFile.png" />
+    <None Remove="Images\PixiParserLogo.png" />
+    <None Remove="Images\PngFile.png" />
     <None Remove="Images\SelectImage.png" />
+    <None Remove="Images\UnknownFile.png" />
     <None Remove="Images\ZoomImage.png" />
     <None Include="..\icon.ico">
       <Pack>True</Pack>
@@ -76,6 +81,7 @@
     <Resource Include="Images\BrightnessImage.png" />
     <Resource Include="Images\Eye-off.png" />
     <Resource Include="Images\Eye.png" />
+    <Resource Include="Images\JpgFile.png" />
     <Resource Include="Images\LineImage.png" />
     <Resource Include="Images\MoveImage.png" />
     <Resource Include="Images\MoveViewportImage.png" />
@@ -83,9 +89,13 @@
     <Resource Include="Images\ColorPickerImage.png" />
     <Resource Include="Images\PixiBotLogo.png" />
     <Resource Include="Images\PixiEditorLogo.png" />
+    <Resource Include="Images\PixiFile.png" />
+    <Resource Include="Images\PixiParserLogo.png" />
+    <Resource Include="Images\PngFile.png" />
     <Resource Include="Images\RectangleImage.png" />
     <Resource Include="Images\SelectImage.png" />
     <Resource Include="Images\transparentbg.png" />
+    <Resource Include="Images\UnknownFile.png" />
     <Resource Include="Images\ZoomImage.png" />
   </ItemGroup>
   <ItemGroup>

+ 36 - 1
PixiEditor/Styles/Titlebar.xaml

@@ -6,7 +6,7 @@
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate TargetType="Button">
-                    <Grid x:Name="LayoutRoot" Background="Transparent" Width="44" Height="30">
+                    <Grid x:Name="LayoutRoot" Background="Transparent" Width="44" Height="35">
                         <TextBlock x:Name="txt" Text="{TemplateBinding Content}" FontFamily="Segoe MDL2 Assets"
                                    FontSize="10"
                                    Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"
@@ -43,4 +43,39 @@
         <Setter Property="Content" Value="&#xE106;" />
     </Style>
 
+    <Style x:Key="CaptionToggleButton" TargetType="ToggleButton">
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="ToggleButton">
+                    <Grid x:Name="LayoutRoot" Background="Transparent" Width="44" Height="35">
+                        <TextBlock x:Name="txt" Text="{TemplateBinding Content}" FontFamily="Segoe MDL2 Assets"
+                                   FontSize="10"
+                                   Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"
+                                   RenderOptions.ClearTypeHint="Auto" TextOptions.TextRenderingMode="Aliased"
+                                   TextOptions.TextFormattingMode="Display" />
+                    </Grid>
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="IsMouseOver" Value="True">
+                            <Setter TargetName="LayoutRoot" Property="Background">
+                                <Setter.Value>
+                                    <SolidColorBrush Color="White" Opacity="0.1" />
+                                </Setter.Value>
+                            </Setter>
+                        </Trigger>
+                        <Trigger Property="IsChecked" Value="True">
+                            <Setter TargetName="LayoutRoot" Property="Background">
+                                <Setter.Value>
+                                    <SolidColorBrush Color="White" Opacity=".3"/>
+                                </Setter.Value>
+                            </Setter>
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+
+    <Style x:Key="PinToggleStyle" TargetType="ToggleButton" BasedOn="{StaticResource CaptionToggleButton}">
+        <Setter Property="Content" Value="&#xE840;"/>
+    </Style>
 </ResourceDictionary>

+ 67 - 5
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -16,6 +16,7 @@ using PixiEditor.Models.Enums;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Parser;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main
 {
@@ -33,6 +34,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public RelayCommand OpenRecentCommand { get; set; }
 
+        public RelayCommand RemoveRecentlyOpenedCommand { get; set; }
+
         public bool HasRecent
         {
             get => hasRecent;
@@ -43,7 +46,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
         }
 
-        public ObservableCollection<string> RecentlyOpened { get; set; } = new ObservableCollection<string>();
+        public RecentlyOpenedCollection RecentlyOpened { get; set; } = new RecentlyOpenedCollection();
 
         public FileViewModel(ViewModelMain owner)
             : base(owner)
@@ -53,13 +56,16 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             OpenFileCommand = new RelayCommand(Open);
             ExportFileCommand = new RelayCommand(ExportFile, CanSave);
             OpenRecentCommand = new RelayCommand(OpenRecent);
+            RemoveRecentlyOpenedCommand = new RelayCommand(RemoveRecentlyOpened);
             Owner.OnStartupEvent += Owner_OnStartupEvent;
-            RecentlyOpened = new ObservableCollection<string>(IPreferences.Current.GetLocalPreference<JArray>(nameof(RecentlyOpened), new JArray()).ToObject<string[]>());
+            RecentlyOpened = new RecentlyOpenedCollection(GetRecentlyOpenedDocuments());
 
             if (RecentlyOpened.Count > 0)
             {
                 HasRecent = true;
             }
+
+            IPreferences.Current.AddCallback("MaxOpenedRecently", UpdateMaxRecentlyOpened);
         }
 
         public void OpenRecent(object parameter)
@@ -85,6 +91,14 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Open((string)parameter);
         }
 
+        public void RemoveRecentlyOpened(object parameter)
+        {
+            if (RecentlyOpened.Contains((string)parameter))
+            {
+                RecentlyOpened.Remove((string)parameter);
+            }
+        }
+
         /// <summary>
         ///     Generates new Layer and sets it as active one.
         /// </summary>
@@ -98,6 +112,11 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
         }
 
+        public void OpenHelloTherePopup()
+        {
+            new HelloTherePopup(this).Show();
+        }
+
         public void NewDocument(int width, int height, bool addBaseLayer = true)
         {
             Owner.BitmapManager.Documents.Add(new Document(width, height));
@@ -138,6 +157,11 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             SaveDocument(parameter: asNew ? "asnew" : null);
         }
 
+        public void OpenAny()
+        {
+            Open((object)null);
+        }
+
         private void Owner_OnStartupEvent(object sender, System.EventArgs e)
         {
             var lastArg = Environment.GetCommandLineArgs().Last();
@@ -147,9 +171,9 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             }
             else
             {
-                if (IPreferences.Current.GetPreference("ShowNewFilePopupOnStartup", true))
+                if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
                 {
-                    OpenNewFilePopup(null);
+                    OpenHelloTherePopup();
                 }
             }
         }
@@ -195,9 +219,13 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             OpenFileDialog dialog = new OpenFileDialog
             {
-                Filter = "All Files|*.*|PixiEditor Files | *.pixi|PNG Files|*.png",
+                Filter =
+                "Any|*.pixi;*.png;*.jpg;*.jpeg;|" +
+                "PixiEditor Files | *.pixi|" +
+                "Image Files|*.png;*.jpg;*.jpeg;",
                 DefaultExt = "pixi"
             };
+
             if ((bool)dialog.ShowDialog())
             {
                 if (Importer.IsSupportedFile(dialog.FileName))
@@ -270,5 +298,39 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             return Owner.BitmapManager.ActiveDocument != null;
         }
+
+        private void UpdateMaxRecentlyOpened(object parameter)
+        {
+            int newAmount = (int)parameter;
+
+            if (newAmount >= RecentlyOpened.Count)
+            {
+                return;
+            }
+
+            var recentlyOpeneds = new List<RecentlyOpenedDocument>(RecentlyOpened.Take(newAmount));
+
+            RecentlyOpened.Clear();
+
+            foreach (var recent in recentlyOpeneds)
+            {
+                RecentlyOpened.Add(recent);
+            }
+        }
+
+        private List<RecentlyOpenedDocument> GetRecentlyOpenedDocuments()
+        {
+            var paths = IPreferences.Current.GetLocalPreference(nameof(RecentlyOpened), new JArray()).ToObject<string[]>()
+                .Take(IPreferences.Current.GetPreference("MaxOpenedRecently", 8));
+
+            List<RecentlyOpenedDocument> documents = new List<RecentlyOpenedDocument>();
+
+            foreach (string path in paths)
+            {
+                documents.Add(new RecentlyOpenedDocument(path));
+            }
+
+            return documents;
+        }
     }
 }

+ 20 - 0
PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs

@@ -15,11 +15,21 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public RelayCommand OpenSettingsWindowCommand { get; set; }
 
+        public RelayCommand OpenShortcutWindowCommand { get; set; }
+
+        public RelayCommand OpenHelloThereWindowCommand { get; set; }
+
+        public ShortcutPopup ShortcutPopup { get; set; }
+
         public MiscViewModel(ViewModelMain owner)
             : base(owner)
         {
             OpenHyperlinkCommand = new RelayCommand(OpenHyperlink);
             OpenSettingsWindowCommand = new RelayCommand(OpenSettingsWindow);
+            OpenShortcutWindowCommand = new RelayCommand(OpenShortcutWindow);
+            OpenHelloThereWindowCommand = new RelayCommand(OpenHelloThereWindow);
+
+            ShortcutPopup = new ShortcutPopup(owner.ShortcutController);
         }
 
         private void OpenSettingsWindow(object parameter)
@@ -43,5 +53,15 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             };
             Process.Start(processInfo);
         }
+
+        private void OpenShortcutWindow(object parameter)
+        {
+            ShortcutPopup.Show();
+        }
+
+        private void OpenHelloThereWindow(object parameter)
+        {
+            new HelloTherePopup(Owner.FileSubViewModel).Show();
+        }
     }
 }

+ 6 - 15
PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs

@@ -8,17 +8,12 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
 {
     public class FileSettings : SettingsGroup
     {
-        private bool showNewFilePopupOnStartup = GetPreference("ShowNewFilePopupOnStartup", true);
+        private bool showStartupWindow = GetPreference(nameof(ShowStartupWindow), true);
 
-        public bool ShowNewFilePopupOnStartup
+        public bool ShowStartupWindow
         {
-            get => showNewFilePopupOnStartup;
-            set
-            {
-                showNewFilePopupOnStartup = value;
-                string name = nameof(ShowNewFilePopupOnStartup);
-                RaiseAndUpdatePreference(name, value);
-            }
+            get => showStartupWindow;
+            set => RaiseAndUpdatePreference(ref showStartupWindow, value);
         }
 
         private long defaultNewFileWidth = GetPreference("DefaultNewFileWidth", 16L);
@@ -47,16 +42,12 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
             }
         }
 
-        private int maxOpenedRecently = GetPreference(nameof(MaxOpenedRecently), 10);
+        private int maxOpenedRecently = GetPreference(nameof(MaxOpenedRecently), 8);
 
         public int MaxOpenedRecently
         {
             get => maxOpenedRecently;
-            set
-            {
-                maxOpenedRecently = value;
-                RaiseAndUpdatePreference(nameof(MaxOpenedRecently), value);
-            }
+            set => RaiseAndUpdatePreference(ref maxOpenedRecently, value);
         }
     }
 }

+ 7 - 0
PixiEditor/ViewModels/SubViewModels/UserPreferences/SettingsGroup.cs

@@ -1,4 +1,5 @@
 using System.ComponentModel;
+using System.Runtime.CompilerServices;
 using PixiEditor.Helpers;
 using PixiEditor.Models.UserPreferences;
 
@@ -25,5 +26,11 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences
             RaisePropertyChanged(name);
             IPreferences.Current.UpdatePreference(name, value);
         }
+
+        protected void RaiseAndUpdatePreference<T>(ref T backingStore, T value, [CallerMemberName]string name = "")
+        {
+            SetProperty(ref backingStore, value, name);
+            IPreferences.Current.UpdatePreference(name, value);
+        }
     }
 }

+ 63 - 53
PixiEditor/ViewModels/ViewModelMain.cs

@@ -17,6 +17,7 @@ using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels.SubViewModels.Main;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels
 {
@@ -109,63 +110,67 @@ namespace PixiEditor.ViewModels
             ViewportSubViewModel = new ViewportViewModel(this);
             ColorsSubViewModel = new ColorsViewModel(this);
             DocumentSubViewModel = new DocumentViewModel(this);
-            MiscSubViewModel = new MiscViewModel(this);
             DiscordViewModel = new DiscordViewModel(this, "764168193685979138");
 #if DEBUG
             DebugSubViewModel = new DebugViewModel(this);
 #endif
 
-            ShortcutController = new ShortcutController
-            {
-                Shortcuts = new List<Shortcut>
-                {
-                    // Tools
-                    CreateToolShortcut<PenTool>(Key.B),
-                    CreateToolShortcut<EraserTool>(Key.E),
-                    CreateToolShortcut<ColorPickerTool>(Key.O),
-                    CreateToolShortcut<RectangleTool>(Key.R),
-                    CreateToolShortcut<CircleTool>(Key.C),
-                    CreateToolShortcut<LineTool>(Key.L),
-                    CreateToolShortcut<FloodFill>(Key.G),
-                    CreateToolShortcut<BrightnessTool>(Key.U),
-                    CreateToolShortcut<MoveTool>(Key.V),
-                    CreateToolShortcut<SelectTool>(Key.M),
-                    CreateToolShortcut<ZoomTool>(Key.Z),
-                    CreateToolShortcut<MoveViewportTool>(Key.H),
-                    new Shortcut(Key.OemPlus, ViewportSubViewModel.ZoomCommand, 115),
-                    new Shortcut(Key.OemMinus, ViewportSubViewModel.ZoomCommand, 85),
-                    new Shortcut(Key.OemOpenBrackets, ToolsSubViewModel.ChangeToolSizeCommand, -1),
-                    new Shortcut(Key.OemCloseBrackets, ToolsSubViewModel.ChangeToolSizeCommand, 1),
-
-                    // Editor
-                    new Shortcut(Key.X, ColorsSubViewModel.SwapColorsCommand),
-                    new Shortcut(Key.Y, UndoSubViewModel.RedoCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.Z, UndoSubViewModel.UndoCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.D, SelectionSubViewModel.DeselectCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.A, SelectionSubViewModel.SelectAllCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.C, ClipboardSubViewModel.CopyCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.V, ClipboardSubViewModel.PasteCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.J, ClipboardSubViewModel.DuplicateCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.X, ClipboardSubViewModel.CutCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.Delete, DocumentSubViewModel.DeletePixelsCommand),
-                    new Shortcut(Key.I, DocumentSubViewModel.OpenResizePopupCommand, modifier: ModifierKeys.Control | ModifierKeys.Shift),
-                    new Shortcut(Key.C, DocumentSubViewModel.OpenResizePopupCommand, "canvas", ModifierKeys.Control | ModifierKeys.Shift),
-                    new Shortcut(Key.F11, SystemCommands.MaximizeWindowCommand),
-
-                    // File
-                    new Shortcut(Key.O, FileSubViewModel.OpenFileCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.S, FileSubViewModel.ExportFileCommand, modifier: ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt),
-                    new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, modifier: ModifierKeys.Control),
-                    new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "AsNew", ModifierKeys.Control | ModifierKeys.Shift),
-                    new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, modifier: ModifierKeys.Control),
-
-                    // Layers
-                    new Shortcut(Key.F2, LayersSubViewModel.RenameLayerCommand, BitmapManager.ActiveDocument?.ActiveLayerGuid),
-
-                    // View
-                    new Shortcut(Key.OemTilde, ViewportSubViewModel.ToggleGridLinesCommand, modifier: ModifierKeys.Control),
-                }
-            };
+            ShortcutController = new ShortcutController(
+                    new ShortcutGroup(
+                        "Tools",
+                        CreateToolShortcut<PenTool>(Key.B, "Select Pen Tool"),
+                        CreateToolShortcut<EraserTool>(Key.E, "Select Eraser Tool"),
+                        CreateToolShortcut<ColorPickerTool>(Key.O, "Select Color Picker Tool"),
+                        CreateToolShortcut<RectangleTool>(Key.R, "Select Rectangle Tool"),
+                        CreateToolShortcut<CircleTool>(Key.C, "Select Circle Tool"),
+                        CreateToolShortcut<LineTool>(Key.L, "Select Line Tool"),
+                        CreateToolShortcut<FloodFill>(Key.G, "Select Flood Fill Tool"),
+                        CreateToolShortcut<BrightnessTool>(Key.U, "Select Brightness Tool"),
+                        CreateToolShortcut<MoveTool>(Key.V, "Select Move Tool"),
+                        CreateToolShortcut<SelectTool>(Key.M, "Select Select Tool"),
+                        CreateToolShortcut<ZoomTool>(Key.Z, "Select Zoom Tool"),
+                        CreateToolShortcut<MoveViewportTool>(Key.H, "Select Viewport Move Tool"),
+                        new Shortcut(Key.OemPlus, ViewportSubViewModel.ZoomCommand, "Zoom in", 115),
+                        new Shortcut(Key.OemMinus, ViewportSubViewModel.ZoomCommand, "Zoom out", 85),
+                        new Shortcut(Key.OemOpenBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Decrease Tool Size", -1),
+                        new Shortcut(Key.OemCloseBrackets, ToolsSubViewModel.ChangeToolSizeCommand, "Increase Tool Size", 1)),
+                    new ShortcutGroup(
+                        "Editor",
+                        new Shortcut(Key.X, ColorsSubViewModel.SwapColorsCommand, "Swap primary and secondary color"),
+                        new Shortcut(Key.Y, UndoSubViewModel.RedoCommand, "Redo", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.Z, UndoSubViewModel.UndoCommand, "Undo", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.D, SelectionSubViewModel.DeselectCommand, "Deselect all command", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.A, SelectionSubViewModel.SelectAllCommand, "Select all command", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.C, ClipboardSubViewModel.CopyCommand, "Copy", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.V, ClipboardSubViewModel.PasteCommand, "Paste", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.J, ClipboardSubViewModel.DuplicateCommand, "Duplicate", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.X, ClipboardSubViewModel.CutCommand, "Cut", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.Delete, DocumentSubViewModel.DeletePixelsCommand, "Delete selected pixels"),
+                        new Shortcut(Key.I, DocumentSubViewModel.OpenResizePopupCommand, "Resize document", modifier: ModifierKeys.Control | ModifierKeys.Shift),
+                        new Shortcut(Key.C, DocumentSubViewModel.OpenResizePopupCommand, "Resize canvas", "canvas", ModifierKeys.Control | ModifierKeys.Shift),
+                        new Shortcut(Key.F11, SystemCommands.MaximizeWindowCommand, "Maximize")),
+                    new ShortcutGroup(
+                        "File",
+                        new Shortcut(Key.O, FileSubViewModel.OpenFileCommand, "Open a Document", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.S, FileSubViewModel.ExportFileCommand, "Export as image", modifier: ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt),
+                        new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save Document", modifier: ModifierKeys.Control),
+                        new Shortcut(Key.S, FileSubViewModel.SaveDocumentCommand, "Save Docuemnt As New", "AsNew", ModifierKeys.Control | ModifierKeys.Shift),
+                        new Shortcut(Key.N, FileSubViewModel.OpenNewFilePopupCommand, "Create new Document", modifier: ModifierKeys.Control)),
+                    new ShortcutGroup(
+                        "Layers",
+                        new Shortcut(Key.F2, LayersSubViewModel.RenameLayerCommand, "Rename active layer", BitmapManager.ActiveDocument?.ActiveLayerGuid)),
+                    new ShortcutGroup(
+                        "View",
+                        new Shortcut(Key.OemTilde, ViewportSubViewModel.ToggleGridLinesCommand, "Toggle gridlines", modifier: ModifierKeys.Control)));
+
+            MiscSubViewModel = new MiscViewModel(this);
+
+            // Add F1 shortcut after MiscSubViewModel is constructed
+            ShortcutController.ShortcutGroups.Add(
+                    new ShortcutGroup(
+                        "Misc",
+                        new Shortcut(Key.F1, MiscSubViewModel.OpenShortcutWindowCommand, "Open the shortcut window", true)));
+
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
         }
 
@@ -193,6 +198,12 @@ namespace PixiEditor.ViewModels
             return new Shortcut(key, ToolsSubViewModel.SelectToolCommand, typeof(T), modifier);
         }
 
+        private Shortcut CreateToolShortcut<T>(Key key, string description, ModifierKeys modifier = ModifierKeys.None)
+            where T : Tool
+        {
+            return new Shortcut(key, ToolsSubViewModel.SelectToolCommand, description, typeof(T), modifier);
+        }
+
         public void CloseWindow(object property)
         {
             if (!(property is CancelEventArgs))
@@ -250,7 +261,6 @@ namespace PixiEditor.ViewModels
             {
                 return false;
             }
-
         }
 
         private void OnStartup(object parameter)

+ 161 - 0
PixiEditor/Views/Dialogs/HelloTherePopup.xaml

@@ -0,0 +1,161 @@
+<Window x:Class="PixiEditor.Views.Dialogs.HelloTherePopup"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:dataHolders="clr-namespace:PixiEditor.Models.DataHolders" xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+        xmlns:sys="clr-namespace:System;assembly=System.Runtime"
+        mc:Ignorable="d"
+        Title="Hello there!" Height="651" Width="651"
+        WindowStyle="None" WindowStartupLocation="CenterScreen">
+
+    <Window.Resources>
+        <converters:EqualityBoolToVisibilityConverter x:Key="EqualBoolToVisibilty"/>
+        <converters:FileExtensionToImageSourceConverter x:Key="FileExtensionToImageSource"/>
+
+        <Style TargetType="TextBlock">
+            <Setter Property="Foreground" Value="White"/>
+            <Setter Property="FontSize" Value="16"/>
+        </Style>
+
+        <Style TargetType="Button" BasedOn="{StaticResource DarkRoundButton}" x:Key="SocialMediaButton">
+            <Setter Property="Width" Value="150"/>
+            <Setter Property="Margin" Value="5,0,5,0"/>
+            <Setter Property="FontSize" Value="18"/>
+            <Setter Property="Height" Value="28"/>
+        </Style>
+    </Window.Resources>
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="35"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}"/>
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+
+    <Grid Background="{StaticResource AccentColor}">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="35" />
+            <RowDefinition Height="*"/>
+        </Grid.RowDefinitions>
+
+        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
+            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
+                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
+                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
+        </DockPanel>
+
+        <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
+            <Grid Grid.Row="1" Margin="0,30,0,0">
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="90"/>
+                    <RowDefinition Height="Auto"/>
+                    <RowDefinition MinHeight="120"/>
+                    <RowDefinition Height="Auto"/>
+                </Grid.RowDefinitions>
+
+                <StackPanel HorizontalAlignment="Center">
+                    <StackPanel Orientation="Horizontal">
+                        <Image Source="../../Images/PixiEditorLogo.png" Height="40" VerticalAlignment="Center"/>
+                        <TextBlock FontSize="40" FontWeight="SemiBold" VerticalAlignment="Center" Margin="10,0,0,0">PixiEditor</TextBlock>
+                    </StackPanel>
+                    <TextBlock HorizontalAlignment="Center" FontSize="20" FontWeight="Medium">v0.2</TextBlock>
+                </StackPanel>
+
+                <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
+                    <Button Style="{StaticResource DarkRoundButton}" Command="{Binding OpenFileCommand}" Width="150" Margin="10">Open</Button>
+                    <Button Style="{StaticResource DarkRoundButton}" Command="{Binding OpenNewFileCommand}" Width="150" Margin="10">New</Button>
+                </StackPanel>
+
+                <StackPanel Grid.Row="2" HorizontalAlignment="Center" Margin="0,30,0,0">
+                    <TextBlock FontSize="23" FontWeight="SemiBold" HorizontalAlignment="Center">Recent Files</TextBlock>
+                    <TextBlock Margin="0,12.5,0,0" Foreground="LightGray" HorizontalAlignment="Center">
+                        <TextBlock.Visibility>
+                            <Binding Path="RecentlyOpened.Count"
+                                     Converter="{StaticResource EqualBoolToVisibilty}">
+                                <Binding.ConverterParameter>
+                                    <sys:Int32/>
+                                </Binding.ConverterParameter>
+                            </Binding>
+                        </TextBlock.Visibility>
+                        So much empty here
+                    </TextBlock>
+                    <ItemsControl ItemsSource="{Binding RecentlyOpened}">
+                        <ItemsControl.ItemTemplate>
+                            <DataTemplate DataType="{x:Type dataHolders:RecentlyOpenedDocument}">
+                                <Grid>
+                                    <StackPanel Margin="8,5,8,0" ToolTip="{Binding FilePath}">
+                                        <Button Margin="0,10,0,0" HorizontalAlignment="Center"
+                                                Width="100" Height="100"
+                                                Command="{Binding DataContext.OpenRecentCommand, RelativeSource={RelativeSource AncestorType=WrapPanel}}"
+                                                CommandParameter="{Binding FilePath}"
+                                                Style="{StaticResource DarkRoundButton}">
+                                            <Image Source="{Binding PreviewBitmap}" Margin="20"/>
+                                        </Button>
+
+                                        <TextBlock Text="{Binding FileName}" Width="110" TextAlignment="Center" TextTrimming="CharacterEllipsis"
+                                                   FontSize="18" Margin="10,10,10,2" HorizontalAlignment="Center" Foreground="White"/>
+                                    </StackPanel>
+                                    <Image Source="{Binding FileExtension, Converter={StaticResource FileExtensionToImageSource}, ConverterParameter=../..}" Width="33"
+                                           ToolTip="{Binding FileExtension}">
+                                        <Image.RenderTransform>
+                                            <TranslateTransform X="38" Y="23"/>
+                                        </Image.RenderTransform>
+                                    </Image>
+                                </Grid>
+                            </DataTemplate>
+                        </ItemsControl.ItemTemplate>
+                        <ItemsControl.ItemsPanel>
+                            <ItemsPanelTemplate>
+                                <WrapPanel HorizontalAlignment="Center"/>
+                            </ItemsPanelTemplate>
+                        </ItemsControl.ItemsPanel>
+                    </ItemsControl>
+                </StackPanel>
+
+                <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,18">
+                    <Button Command="{Binding OpenHyperlinkCommand}" CommandParameter="https://discord.gg/tzkQFDkqQS"
+                            Content="Discord">
+                        <Button.Style>
+                            <Style TargetType="Button" BasedOn="{StaticResource SocialMediaButton}">
+                                <Style.Triggers>
+                                    <Trigger Property="IsMouseOver" Value="True">
+                                        <Setter Property="Background" Value="#7289DA"/>
+                                    </Trigger>
+                                </Style.Triggers>
+                            </Style>
+                        </Button.Style>
+                    </Button>
+                    <Button Command="{Binding OpenHyperlinkCommand}" CommandParameter="https://reddit.com/r/PixiEditor"
+                            Content="r/PixiEditor">
+                        <Button.Style>
+                            <Style TargetType="Button" BasedOn="{StaticResource SocialMediaButton}">
+                                <Style.Triggers>
+                                    <Trigger Property="IsMouseOver" Value="True">
+                                        <Setter Property="Background" Value="#FF4500"/>
+                                    </Trigger>
+                                </Style.Triggers>
+                            </Style>
+                        </Button.Style>
+                    </Button>
+                    <Button Command="{Binding OpenHyperlinkCommand}" CommandParameter="https://github.com/PixiEditor/PixiEditor"
+                            Content="GitHub">
+                        <Button.Style>
+                            <Style TargetType="Button" BasedOn="{StaticResource SocialMediaButton}">
+                                <Style.Triggers>
+                                    <Trigger Property="IsMouseOver" Value="True">
+                                        <Setter Property="Background" Value="White"/>
+                                        <Setter Property="Foreground" Value="Black"/>
+                                    </Trigger>
+                                </Style.Triggers>
+                            </Style>
+                        </Button.Style>
+                    </Button>
+                </StackPanel>
+            </Grid>
+        </ScrollViewer>
+    </Grid>
+</Window>

+ 115 - 0
PixiEditor/Views/Dialogs/HelloTherePopup.xaml.cs

@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Windows;
+using System.Windows.Input;
+using Newtonsoft.Json.Linq;
+using PixiEditor.Helpers;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels.SubViewModels.Main;
+
+namespace PixiEditor.Views.Dialogs
+{
+    /// <summary>
+    /// Interaction logic for HelloTherePopup.xaml.
+    /// </summary>
+    public partial class HelloTherePopup : Window
+    {
+        public RecentlyOpenedCollection RecentlyOpened { get => FileViewModel.RecentlyOpened; }
+
+        public static readonly DependencyProperty FileViewModelProperty =
+            DependencyProperty.Register(nameof(FileViewModel), typeof(FileViewModel), typeof(HelloTherePopup));
+
+        public FileViewModel FileViewModel { get => (FileViewModel)GetValue(FileViewModelProperty); set => SetValue(FileViewModelProperty, value); }
+
+        public static readonly DependencyProperty RecentlyOpenedEmptyProperty =
+            DependencyProperty.Register(nameof(RecentlyOpenedEmpty), typeof(bool), typeof(HelloTherePopup));
+
+        public bool RecentlyOpenedEmpty { get => (bool)GetValue(RecentlyOpenedEmptyProperty); set => SetValue(RecentlyOpenedEmptyProperty, value); }
+
+        public RelayCommand OpenFileCommand { get; set; }
+
+        public RelayCommand OpenNewFileCommand { get; set; }
+
+        public RelayCommand OpenRecentCommand { get; set; }
+
+        public RelayCommand OpenHyperlinkCommand { get => FileViewModel.Owner.MiscSubViewModel.OpenHyperlinkCommand; }
+
+        public HelloTherePopup(FileViewModel fileViewModel)
+        {
+            DataContext = this;
+            FileViewModel = fileViewModel;
+
+            OpenFileCommand = new RelayCommand(OpenFile);
+            OpenNewFileCommand = new RelayCommand(OpenNewFile);
+            OpenRecentCommand = new RelayCommand(OpenRecent);
+
+            RecentlyOpenedEmpty = RecentlyOpened.Count == 0;
+            RecentlyOpened.CollectionChanged += RecentlyOpened_CollectionChanged;
+
+            InitializeComponent();
+
+            if (RecentlyOpenedEmpty)
+            {
+                Height = 450;
+                Width = 522;
+            }
+            else if (RecentlyOpened.Count < 7)
+            {
+                Height = 656;
+                Width = 545;
+            }
+        }
+
+        protected override void OnDeactivated(EventArgs e)
+        {
+            CloseIfRelease();
+        }
+
+        private void RecentlyOpened_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+        {
+            RecentlyOpenedEmpty = FileViewModel.RecentlyOpened.Count == 0;
+        }
+
+        [Conditional("RELEASE")]
+        private void CloseIfRelease()
+        {
+            Close();
+        }
+
+        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+        {
+            e.CanExecute = true;
+        }
+
+        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.CloseWindow(this);
+        }
+
+        private void OpenFile(object parameter)
+        {
+            Close();
+            FileViewModel.OpenAny();
+        }
+
+        private void OpenNewFile(object parameter)
+        {
+            Close();
+            FileViewModel.OpenNewFilePopup(parameter);
+        }
+
+        private void OpenRecent(object parameter)
+        {
+            FileViewModel.OpenRecent(parameter);
+            Close();
+        }
+    }
+}

+ 4 - 4
PixiEditor/Views/Dialogs/SettingsWindow.xaml

@@ -57,11 +57,11 @@
                 <StackPanel Orientation="Vertical">
                     <Label Content="File" Style="{StaticResource Header1}"/>
                     <StackPanel Orientation="Vertical" Margin="50 0 50 0">
-                        <CheckBox Content="Show New File dialog on startup" 
-                                  IsChecked="{Binding SettingsSubViewModel.File.ShowNewFilePopupOnStartup}"/>
+                        <CheckBox Content="Show Startup Window" 
+                                  IsChecked="{Binding SettingsSubViewModel.File.ShowStartupWindow}"/>
                         <StackPanel Orientation="Horizontal" Margin="0,10,0,0">
-                            <Label Content="Max Saved Opened Recently:" ToolTip="How many documents are shown under File > Recent. Default: 10" Style="{StaticResource BaseLabel}"/>
-                            <views:NumberInput FontSize="16" Value="{Binding SettingsSubViewModel.File.MaxOpenedRecently}" Width="40"/>
+                            <Label Content="Max Saved Opened Recently:" ToolTip="How many documents are shown under File > Recent. Default: 8" Style="{StaticResource BaseLabel}"/>
+                            <views:NumberInput FontSize="16" Value="{Binding SettingsSubViewModel.File.MaxOpenedRecently, Mode=TwoWay}" Width="40"/>
                         </StackPanel>
                         <Label Content="Default new file size:" Style="{StaticResource Header2}" Margin="0 20 0 20"/>
                         <StackPanel Orientation="Horizontal" Margin="40,0,0,0">

+ 123 - 0
PixiEditor/Views/Dialogs/ShortcutPopup.xaml

@@ -0,0 +1,123 @@
+<Window x:Class="PixiEditor.Views.Dialogs.ShortcutPopup"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:s="clr-namespace:System;assembly=mscorlib"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:PixiEditor.Views.Dialogs"
+        xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+        xmlns:shortcuts="clr-namespace:PixiEditor.Models.Controllers.Shortcuts" xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls"
+        mc:Ignorable="d"
+        Title="ShortcutPopup" Height="815" Width="620" WindowStyle="None"
+        MinHeight="400" MinWidth="350" Topmost="{Binding IsTopmost}">
+    <Window.Resources>
+        <converters:KeyToStringConverter x:Key="KeyToStringConverter"/>
+        <BoolToVisibilityConverter x:Key="BoolToVisibilty"/>
+        
+        <Style TargetType="Border" x:Key="KeyBorder">
+            <Setter Property="BorderThickness" Value="1"/>
+            <Setter Property="BorderBrush" Value="{StaticResource BrighterAccentColor}"/>
+            <Setter Property="Background" Value="{StaticResource BrighterAccentColor}"/>
+            <Setter Property="CornerRadius" Value="5"/>
+            <Setter Property="Margin" Value="0,3,5,3"/>
+        </Style>
+        <Style TargetType="Border" x:Key="KeyBorderLast" BasedOn="{StaticResource KeyBorder}">
+            <Setter Property="Margin" Value="0,3,0,3"/>
+        </Style>
+        
+        <Style TargetType="TextBlock">
+            <Setter Property="Foreground" Value="White"/>
+            <Setter Property="FontSize" Value="16"/>
+        </Style>
+        <Style TargetType="TextBlock" x:Key="KeyBorderText" BasedOn="{StaticResource {x:Type TextBlock}}">
+            <Setter Property="FontWeight" Value="Medium"/>
+            <Setter Property="FontSize" Value="14"/>
+            <Setter Property="Margin" Value="4,0,4,0"/>
+        </Style>
+
+        <Style TargetType="ListView">
+            <Setter Property="Background" Value="Transparent"/>
+            <Setter Property="BorderThickness" Value="0"/>
+        </Style>
+    </Window.Resources>
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="35"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}"/>
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+        <CommandBinding Command="{x:Static SystemCommands.MinimizeWindowCommand}"
+                        CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed_Minimize" />
+    </Window.CommandBindings>
+
+    <Grid Background="{StaticResource AccentColor}">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="35" />
+            <RowDefinition Height="Auto"/>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition />
+        </Grid.RowDefinitions>
+
+        <DockPanel Grid.Row="0" Background="{StaticResource MainColor}">
+            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
+                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
+                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
+            <Button DockPanel.Dock="Right" HorizontalAlignment="Right"  Style="{StaticResource MinimizeButtonStyle}" 
+                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Minimize"
+                        Command="{x:Static SystemCommands.MinimizeWindowCommand}" />
+            <ToggleButton HorizontalAlignment="Right" IsChecked="{Binding IsTopmost, Mode=TwoWay}" Style="{StaticResource PinToggleStyle}"
+                          WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Makes this window always on top"/>
+        </DockPanel>
+
+        <TextBlock Grid.Row="1" Margin="5,0,0,0" FontSize="25" HorizontalAlignment="Center">Shortcuts</TextBlock>
+
+        <ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto">
+            <WrapPanel HorizontalAlignment="Center">
+                <ItemsControl ItemsSource="{Binding Controller.ShortcutGroups}" Background="Transparent">
+                    <ItemsControl.ItemTemplate>
+                        <DataTemplate DataType="{x:Type shortcuts:ShortcutGroup}">
+                            <StackPanel Visibility="{Binding IsVisible, Converter={StaticResource BoolToVisibilty}}">
+                                <TextBlock Text="{Binding Name}" Foreground="White" FontSize="18" FontWeight="Medium" Margin="10,8,0,0"/>
+                                <ItemsControl ItemsSource="{Binding Shortcuts}">
+                                    <ItemsControl.ItemTemplate>
+                                        <DataTemplate DataType="{x:Type shortcuts:Shortcut}">
+                                            <StackPanel Orientation="Horizontal" Margin="20,0,0,0">
+                                                <ItemsControl ItemsSource="{Binding Modifiers}">
+                                                    <ItemsControl.ItemTemplate>
+                                                        <DataTemplate DataType="{x:Type ModifierKeys}">
+                                                            <Border Style="{StaticResource KeyBorder}">
+                                                                <TextBlock Text="{Binding BindsDirectlyToSource=True, Converter={StaticResource KeyToStringConverter}}" Style="{StaticResource KeyBorderText}"/>
+                                                            </Border>
+                                                        </DataTemplate>
+                                                    </ItemsControl.ItemTemplate>
+                                                    <ItemsControl.ItemsPanel>
+                                                        <ItemsPanelTemplate>
+                                                            <StackPanel Orientation="Horizontal"/>
+                                                        </ItemsPanelTemplate>
+                                                    </ItemsControl.ItemsPanel>
+                                                </ItemsControl>
+                                                <Border Style="{StaticResource KeyBorderLast}">
+                                                    <TextBlock Text="{Binding ShortcutKey, Converter={StaticResource KeyToStringConverter}}" Style="{StaticResource KeyBorderText}"/>
+                                                </Border>
+
+                                                <TextBlock Text="{Binding Description}" Foreground="#FFEEEEEE"  FontSize="14" Margin="8,0,0,0"/>
+                                            </StackPanel>
+                                        </DataTemplate>
+                                    </ItemsControl.ItemTemplate>
+                                </ItemsControl>
+                            </StackPanel>
+                        </DataTemplate>
+                    </ItemsControl.ItemTemplate>
+                    <ItemsControl.ItemsPanel>
+                        <ItemsPanelTemplate>
+                            <WrapPanel ItemWidth="300"/>
+                        </ItemsPanelTemplate>
+                    </ItemsControl.ItemsPanel>
+                </ItemsControl>
+            </WrapPanel>
+        </ScrollViewer>
+    </Grid>
+</Window>

+ 63 - 0
PixiEditor/Views/Dialogs/ShortcutPopup.xaml.cs

@@ -0,0 +1,63 @@
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using PixiEditor.Models.Controllers.Shortcuts;
+
+namespace PixiEditor.Views.Dialogs
+{
+    /// <summary>
+    /// Interaction logic for ShortcutPopup.xaml.
+    /// </summary>
+    public partial class ShortcutPopup : Window
+    {
+        public static readonly DependencyProperty ControllerProperty =
+            DependencyProperty.Register(nameof(Controller), typeof(ShortcutController), typeof(ShortcutPopup));
+
+        public ShortcutController Controller { get => (ShortcutController)GetValue(ControllerProperty); set => SetValue(ControllerProperty, value); }
+
+        public static readonly DependencyProperty IsTopmostProperty =
+            DependencyProperty.Register(nameof(IsTopmost), typeof(bool), typeof(ShortcutPopup));
+
+        public bool IsTopmost { get => (bool)GetValue(IsTopmostProperty); set => SetValue(IsTopmostProperty, value); }
+
+        public ShortcutPopup(ShortcutController controller)
+        {
+            DataContext = this;
+            InitializeComponent();
+            Controller = controller;
+        }
+
+        protected override void OnClosing(CancelEventArgs e)
+        {
+            e.Cancel = true;
+
+            Hide();
+        }
+
+        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+        {
+            e.CanExecute = true;
+        }
+
+        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.CloseWindow(this);
+        }
+
+        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);
+        }
+    }
+}

+ 11 - 5
PixiEditor/Views/MainWindow.xaml

@@ -11,7 +11,7 @@
         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:avalonDockTheme="clr-namespace:PixiEditor.Styles.AvalonDock" d:DataContext="{d:DesignInstance Type=vm:ViewModelMain}" xmlns:dataHolders="clr-namespace:PixiEditor.Models.DataHolders"
         mc:Ignorable="d" WindowStyle="None" Initialized="MainWindow_Initialized"
         Title="PixiEditor" Name="mainWindow" Height="1000" Width="1600" Background="{StaticResource MainColor}"
         WindowStartupLocation="CenterScreen" WindowState="Maximized">
@@ -86,11 +86,16 @@
                     <MenuItem Header="_Open" InputGestureText="Ctrl+O" Command="{Binding FileSubViewModel.OpenFileCommand}" />
                     <MenuItem Header="_Recent" ItemsSource="{Binding FileSubViewModel.RecentlyOpened}" x:Name="recentItemMenu" IsEnabled="{Binding FileSubViewModel.HasRecent}">
                         <MenuItem.ItemContainerStyle>
-                            <Style TargetType="MenuItem" BasedOn="{StaticResource menuItemStyle}">
+                            <Style TargetType="MenuItem">
                                 <Setter Property="Command" Value="{Binding ElementName=recentItemMenu, Path=DataContext.FileSubViewModel.OpenRecentCommand}"/>
-                                <Setter Property="CommandParameter" Value="{Binding}"/>
+                                <Setter Property="CommandParameter" Value="{Binding FilePath}"/>
                             </Style>
                         </MenuItem.ItemContainerStyle>
+                        <MenuItem.ItemTemplate>
+                            <DataTemplate DataType="{x:Type dataHolders:RecentlyOpenedDocument}">
+                                <TextBlock Text="{Binding FilePath}"/>
+                            </DataTemplate>
+                        </MenuItem.ItemTemplate>
                     </MenuItem>
                     <MenuItem Header="_Save" InputGestureText="Ctrl+S" Command="{Binding FileSubViewModel.SaveDocumentCommand}" />
                     <MenuItem Header="_Save As..." InputGestureText="Ctrl+Shift+S"
@@ -129,14 +134,15 @@
                 <MenuItem Header="_View">
                     <MenuItem Header="_Show Grid Lines" IsChecked="{Binding ViewportSubViewModel.GridLinesEnabled, Mode=TwoWay}"
                               IsCheckable="True" InputGestureText="Ctrl+`"/>
+                    <MenuItem Header="_Open Startup Window" ToolTip="Hello there!"
+                              Command="{Binding MiscSubViewModel.OpenHelloThereWindowCommand}"/>
                 </MenuItem>
                 <MenuItem Header="_Help">
                     <MenuItem Header="_Documentation" Command="{Binding MiscSubViewModel.OpenHyperlinkCommand}"
                               CommandParameter="https://github.com/PixiEditor/PixiEditor/wiki"/>
                     <MenuItem Header="_Repository" Command="{Binding MiscSubViewModel.OpenHyperlinkCommand}"
                               CommandParameter="https://github.com/PixiEditor/PixiEditor"/>
-                    <MenuItem Header="_Shortcuts" Command="{Binding MiscSubViewModel.OpenHyperlinkCommand}"
-                              CommandParameter="https://github.com/PixiEditor/PixiEditor/wiki/Shortcuts"/>
+                    <MenuItem Header="_Shortcuts" Command="{Binding MiscSubViewModel.OpenShortcutWindowCommand}"/>
                     <Separator/>
                     <MenuItem Header="_License" Command="{Binding MiscSubViewModel.OpenHyperlinkCommand}"
                               CommandParameter="https://github.com/PixiEditor/PixiEditor/blob/master/LICENSE"/>

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

@@ -2,7 +2,6 @@
 using System.ComponentModel;
 using System.Diagnostics;
 using System.IO;
-using System.Reflection;
 using System.Windows;
 using System.Windows.Input;
 using Microsoft.Extensions.DependencyInjection;
@@ -35,6 +34,7 @@ namespace PixiEditor
             MaxHeight = SystemParameters.MaximizedPrimaryScreenHeight;
             viewModel = (ViewModelMain)DataContext;
             viewModel.CloseAction = Close;
+            Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
         }
 
         protected override void OnClosing(CancelEventArgs e)

+ 3 - 3
PixiEditorTests/ModelsTests/ControllersTests/ShortcutControllerTests.cs

@@ -55,7 +55,7 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
             RelayCommand shortcutCommand = new RelayCommand(arg => { result = (int)arg; });
 
             ShortcutController controller = GenerateStandardShortcutController(Key.A, ModifierKeys.None, shortcutCommand);
-            controller.Shortcuts.Add(new Shortcut(Key.A, shortcutCommand, 1, ModifierKeys.Control));
+            controller.ShortcutGroups.Add(new ShortcutGroup(string.Empty, new Shortcut(Key.A, shortcutCommand, 1, ModifierKeys.Control)));
 
             controller.KeyPressed(Key.A, ModifierKeys.Control);
             Assert.Equal(1, result);
@@ -68,13 +68,13 @@ namespace PixiEditorTests.ModelsTests.ControllersTests
 
             Assert.Null(controller.LastShortcut);
             controller.KeyPressed(Key.A, ModifierKeys.None);
-            Assert.Equal(controller.Shortcuts[0], controller.LastShortcut);
+            Assert.Equal(controller.ShortcutGroups[0].Shortcuts[0], controller.LastShortcut);
         }
 
         private static ShortcutController GenerateStandardShortcutController(Key shortcutKey, ModifierKeys modifiers, RelayCommand shortcutCommand)
         {
             ShortcutController controller = new ShortcutController();
-            controller.Shortcuts.Add(new Shortcut(shortcutKey, shortcutCommand, 0, modifiers));
+            controller.ShortcutGroups.Add(new ShortcutGroup(string.Empty, new Shortcut(shortcutKey, shortcutCommand, 0, modifiers)));
             ShortcutController.BlockShortcutExecution = false;
             return controller;
         }

+ 1 - 1
PixiEditorTests/ModelsTests/DataHoldersTests/DocumentTests.cs

@@ -305,7 +305,7 @@ namespace PixiEditorTests.ModelsTests.DataHoldersTests
 
             document.DocumentFilePath = testFilePath;
 
-            Assert.Contains(viewModel.FileSubViewModel.RecentlyOpened, x => x == testFilePath);
+            Assert.Contains(viewModel.FileSubViewModel.RecentlyOpened, x => x.FilePath == testFilePath);
         }
 
         [Fact]

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

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

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

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

+ 1 - 1
PixiEditorTests/ViewModelsTests/ViewModelMainTests.cs

@@ -34,7 +34,7 @@ namespace PixiEditorTests.ViewModelsTests
 
             Assert.NotNull(viewModel.ChangesController);
             Assert.NotNull(viewModel.ShortcutController);
-            Assert.NotEmpty(viewModel.ShortcutController.Shortcuts);
+            Assert.NotEmpty(viewModel.ShortcutController.ShortcutGroups);
             Assert.NotNull(viewModel.BitmapManager);
             Assert.Equal(viewModel, ViewModelMain.Current);
         }