Browse Source

Added Shortcut Popup

CPKreuz 4 years ago
parent
commit
fca0b2521c

+ 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);
+    }
+}

+ 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;
+        }
+    }
+}

+ 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>

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

@@ -15,11 +15,18 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
         public RelayCommand OpenSettingsWindowCommand { get; set; }
 
+        public RelayCommand OpenShortcutWindowCommand { 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);
+
+            ShortcutPopup = new ShortcutPopup(owner.ShortcutController);
         }
 
         private void OpenSettingsWindow(object parameter)
@@ -43,5 +50,10 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             };
             Process.Start(processInfo);
         }
+
+        private void OpenShortcutWindow(object parameter)
+        {
+            ShortcutPopup.Show();
+        }
     }
 }

+ 56 - 52
PixiEditor/ViewModels/ViewModelMain.cs

@@ -109,63 +109,61 @@ 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);
+
             BitmapManager.PrimaryColor = ColorsSubViewModel.PrimaryColor;
         }
 
@@ -193,6 +191,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))

+ 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);
+        }
+    }
+}

+ 1 - 2
PixiEditor/Views/MainWindow.xaml

@@ -135,8 +135,7 @@
                               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/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);
         }