Browse Source

Added Template card, mapping wip

Krzysztof Krysiński 2 years ago
parent
commit
cd2d6f6ba3

+ 105 - 0
src/PixiEditor/Data/ShortcutActionMaps/AsepriteShortcutMap.json

@@ -0,0 +1,105 @@
+{
+    "" : "PixiEditor.Shortcuts.Provider.ImportDefault",
+    "" : "PixiEditor.Shortcuts.Provider.ImportInstallation",
+    "" : "PixiEditor.Shortcuts.Reset",
+    "" : "PixiEditor.Shortcuts.Export",
+    "" : "PixiEditor.Shortcuts.Import",
+    "" : "PixiEditor.Shortcuts.OpenTemplatePopup",
+    "" : "PixiEditor.Clipboard.Cut",
+    "" : "PixiEditor.Clipboard.Paste",
+    "" : "PixiEditor.Clipboard.PasteColor",
+    "" : "PixiEditor.Clipboard.Copy",
+    "" : "PixiEditor.Colors.ReplaceColors",
+    "" : "PixiEditor.Colors.OpenPaletteBrowser",
+    "" : "PixiEditor.Colors.ImportPalette",
+    "" : "PixiEditor.Colors.SelectFirstPaletteColor",
+    "" : "PixiEditor.Colors.SelectSecondPaletteColor",
+    "" : "PixiEditor.Colors.SelectThirdPaletteColor",
+    "" : "PixiEditor.Colors.SelectFourthPaletteColor",
+    "" : "PixiEditor.Colors.SelectFifthPaletteColor",
+    "" : "PixiEditor.Colors.SelectSixthPaletteColor",
+    "" : "PixiEditor.Colors.SelectSeventhPaletteColor",
+    "" : "PixiEditor.Colors.SelectEighthPaletteColor",
+    "" : "PixiEditor.Colors.SelectNinthPaletteColor",
+    "" : "PixiEditor.Colors.SelectTenthPaletteColor",
+    "" : "PixiEditor.Colors.Swap",
+    "" : "PixiEditor.Colors.RemoveSwatch",
+    "" : "PixiEditor.Colors.SelectColor",
+    "" : "PixiEditor.File.New",
+    "" : "PixiEditor.File.Open",
+    "" : "PixiEditor.File.OpenRecent",
+    "" : "PixiEditor.File.Save",
+    "" : "PixiEditor.File.SaveAsNew",
+    "" : "PixiEditor.File.Export",
+    "" : "PixiEditor.Layer.DeleteSelected",
+    "" : "PixiEditor.Layer.DeleteAllSelected",
+    "" : "PixiEditor.Layer.NewFolder",
+    "" : "PixiEditor.Layer.NewLayer",
+    "" : "PixiEditor.Layer.ToggleLockTransparency",
+    "" : "PixiEditor.Layer.OpacitySliderDragStarted",
+    "" : "PixiEditor.Layer.OpacitySliderDragged",
+    "" : "PixiEditor.Layer.OpacitySliderDragEnded",
+    "" : "PixiEditor.Layer.DuplicateSelectedLayer",
+    "" : "PixiEditor.Layer.CreateMask",
+    "" : "PixiEditor.Layer.DeleteMask",
+    "" : "PixiEditor.Layer.MoveSelectedMemberUpwards",
+    "" : "PixiEditor.Layer.MoveSelectedMemberDownwards",
+    "" : "PixiEditor.Layer.MergeSelected",
+    "" : "PixiEditor.Layer.MergeWithAbove",
+    "" : "PixiEditor.Layer.MergeWithBelow",
+    "" : "PixiEditor.Links.OpenDocumentation",
+    "" : "PixiEditor.Links.OpenWebsite",
+    "" : "PixiEditor.Links.OpenRepository",
+    "" : "PixiEditor.Links.OpenLicense",
+    "" : "PixiEditor.Links.OpenOtherLicenses",
+    "" : "PixiEditor.Search.Toggle",
+    "" : "PixiEditor.Selection.SelectAll",
+    "" : "PixiEditor.Selection.Clear",
+    "" : "PixiEditor.Selection.TransformArea",
+    "" : "PixiEditor.Stylus.TogglePenMode",
+    "" : "PixiEditor.Stylus.StylusOutOfRange",
+    "" : "PixiEditor.Stylus.StylusSystemGesture",
+    "" : "PixiEditor.Stylus.StylusDown",
+    "" : "PixiEditor.Stylus.StylusUp",
+    "" : "PixiEditor.Tools.ApplyTransform",
+    "" : "PixiEditor.Tools.SelectTool",
+    "" : "PixiEditor.Tools.IncreaseSize",
+    "" : "PixiEditor.Tools.DecreaseSize",
+    "" : "PixiEditor.Undo.Redo",
+    "" : "PixiEditor.Undo.Undo",
+    "" : "PixiEditor.Restart",
+    "" : "PixiEditor.View.ToggleGrid",
+    "" : "PixiEditor.View.ZoomIn",
+    "" : "PixiEditor.View.Zoomout",
+    "" : "PixiEditor.Window.CreateNewViewport",
+    "" : "PixiEditor.Window.OpenSettingsWindow",
+    "" : "PixiEditor.Window.OpenStartupWindow",
+    "" : "PixiEditor.Window.OpenShortcutWindow",
+    "" : "PixiEditor.Window.OpenNavigationWindow",
+    "" : "PixiEditor.Document.ClipCanvas",
+    "" : "PixiEditor.Document.ToggleVerticalSymmetryAxis",
+    "" : "PixiEditor.Document.ToggleHorizontalSymmetryAxis",
+    "" : "PixiEditor.Document.DragSymmetry",
+    "" : "PixiEditor.Document.StartDragSymmetry",
+    "" : "PixiEditor.Document.EndDragSymmetry",
+    "" : "PixiEditor.Document.DeletePixels",
+    "" : "PixiEditor.Document.ResizeDocument",
+    "" : "PixiEditor.Document.ResizeCanvas",
+    "" : "PixiEditor.Document.CenterContent",
+    "" : "PixiEditor.Tools.Select.BrightnessToolViewModel",
+    "" : "PixiEditor.Tools.Select.ColorPickerToolViewModel",
+    "" : "PixiEditor.Tools.Select.EllipseToolViewModel",
+    "" : "PixiEditor.Tools.Select.EraserToolViewModel",
+    "" : "PixiEditor.Tools.Select.FloodFillToolViewModel",
+    "" : "PixiEditor.Tools.Select.LassoToolViewModel",
+    "" : "PixiEditor.Tools.Select.LineToolViewModel",
+    "" : "PixiEditor.Tools.Select.MagicWandToolViewModel",
+    "" : "PixiEditor.Tools.Select.MoveToolViewModel",
+    "" : "PixiEditor.Tools.Select.MoveViewportToolViewModel",
+    "" : "PixiEditor.Tools.Select.PenToolViewModel",
+    "" : "PixiEditor.Tools.Select.RectangleToolViewModel",
+    "" : "PixiEditor.Tools.Select.RotateViewportToolViewModel",
+    "" : "PixiEditor.Tools.Select.SelectToolViewModel",
+    "Tools.zoom" : "PixiEditor.Tools.Select.ZoomToolViewModel"
+    }
+    

BIN
src/PixiEditor/Images/TemplateLogos/Aseprite-Hover.png


BIN
src/PixiEditor/Images/TemplateLogos/Aseprite.png


+ 15 - 2
src/PixiEditor/Models/Commands/CommandController.cs

@@ -2,11 +2,13 @@
 using System.Reflection;
 using System.Windows.Media;
 using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json;
 using PixiEditor.Models.Commands.Attributes;
 using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.ViewModels.SubViewModels.Tools;
 using CommandAttribute = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
@@ -14,7 +16,7 @@ namespace PixiEditor.Models.Commands;
 
 internal class CommandController
 {
-    private readonly ShortcutFile shortcutFile;
+    private ShortcutFile shortcutFile;
 
     public static CommandController Current { get; private set; }
 
@@ -92,7 +94,18 @@ internal class CommandController
 
     public void Init(IServiceProvider serviceProvider)
     {
-        ShortcutsTemplate template = shortcutFile.LoadTemplate();
+        ShortcutsTemplate template = new();
+        try
+        {
+            template = shortcutFile.LoadTemplate();
+        }
+        catch (JsonException)
+        {
+            File.Move(shortcutFile.Path, $"{shortcutFile.Path}.corrupted", true);
+            shortcutFile = new ShortcutFile(ShortcutsPath, this);
+            template = shortcutFile.LoadTemplate();
+            NoticeDialog.Show("Shortcuts file was corrupted, resetting to default.", "Corrupted shortcuts file");
+        }
 
         Type[] allTypesInPixiEditorAssembly = typeof(CommandController).Assembly.GetTypes();
 

+ 18 - 2
src/PixiEditor/Models/Commands/Templates/Providers/AsepriteProvider.cs

@@ -1,4 +1,6 @@
-using System.Windows.Input;
+using System.IO;
+using System.Windows.Input;
+using PixiEditor.Models.Commands.Templates.Parsers;
 
 namespace PixiEditor.Models.Commands.Templates;
 
@@ -6,11 +8,25 @@ internal partial class ShortcutProvider
 {
     public static AsepriteProvider Aseprite { get; } = new();
 
-    internal class AsepriteProvider : ShortcutProvider, IShortcutDefaults
+    internal class AsepriteProvider : ShortcutProvider, IShortcutDefaults, IShortcutInstallation
     {
+        private static string InstallationPath { get; } = Path.Combine(
+            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Aseprite", "user.aseprite-keys");
+
+        private AsepriteKeysParser _parser;
+        
         public AsepriteProvider() : base("Aseprite")
         {
+            _parser = new AsepriteKeysParser("AsepriteShortcutMap.json");
             LogoPath = "/Images/TemplateLogos/Aseprite.png";
+            HoverLogoPath = "/Images/TemplateLogos/Aseprite-Hover.png";
+        }
+
+        public bool InstallationPresent => File.Exists(InstallationPath);
+        
+        public ShortcutsTemplate GetInstalledShortcuts()
+        {
+            return _parser.Parse(InstallationPath);
         }
 
         public List<Shortcut> DefaultShortcuts { get; } = new()

+ 51 - 0
src/PixiEditor/Models/Commands/Templates/Providers/Parsers/AsepriteKeysParser.cs

@@ -0,0 +1,51 @@
+using System.IO;
+using System.Xml;
+
+namespace PixiEditor.Models.Commands.Templates.Parsers;
+
+/// <summary>
+///     Aseprite uses XML (under .aseprite-keys file) to store keybindings.
+/// This class is used to load and parse this file into <see cref="ShortcutsTemplate"/> class.
+///
+/// Aseprite keys consists of 5 sections:
+/// <list type="">
+///     <item>Commands</item>
+///     <item>Tools</item>
+///     <item>QuickTools</item>
+///     <item>Actions</item>
+///     <item>Actions</item>
+/// </list>
+///  
+/// We are only interested in Commands and Tools sections, because actions (like binding Left Mouse Button to some shortcut)
+/// are not yet supported by us.
+/// </summary>
+public class AsepriteKeysParser : KeysParser
+{
+    public AsepriteKeysParser(string mapFileName) : base(mapFileName)
+    {
+    }
+    
+    public override ShortcutsTemplate Parse(string path)
+    {
+        if (!File.Exists(path))
+        {
+            throw new FileNotFoundException("File not found", path);
+        }
+
+        if (Path.GetExtension(path) != ".aseprite-keys")
+        {
+            throw new ArgumentException("File is not aseprite-keys file", nameof(path));
+        }
+        
+        return LoadAndParse(path);
+    }
+
+    private static ShortcutsTemplate LoadAndParse(string path)
+    {
+        XmlDocument doc = new XmlDocument();
+        doc.Load(path);
+        
+        ShortcutsTemplate template = new ShortcutsTemplate();
+        return template;
+    }
+}

+ 34 - 0
src/PixiEditor/Models/Commands/Templates/Providers/Parsers/KeysParser.cs

@@ -0,0 +1,34 @@
+using System.IO;
+using Newtonsoft.Json;
+
+namespace PixiEditor.Models.Commands.Templates.Parsers;
+
+public abstract class KeysParser
+{
+    public string MapFileName { get; }
+
+    private static string _fullMapFilePath;
+
+    public Dictionary<string, string> Map => _cachedMap ??= LoadKeysMap();
+    private Dictionary<string, string> _cachedMap;
+
+    public KeysParser(string mapFileName)
+    {
+        _fullMapFilePath = Path.Combine("Data", "ShortcutActionMaps", mapFileName);
+        if (!File.Exists(_fullMapFilePath))
+        {
+            throw new FileNotFoundException($"Keys map file '{_fullMapFilePath}' not found.");
+        }
+        
+        MapFileName = mapFileName;
+    }
+    
+    public abstract ShortcutsTemplate Parse(string filePath);
+    
+    private Dictionary<string, string> LoadKeysMap()
+    {
+        string text = File.ReadAllText(_fullMapFilePath);
+        var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(text);
+        return dict;
+    }
+}

+ 2 - 3
src/PixiEditor/Models/Commands/Templates/ShortcutProvider.cs

@@ -1,12 +1,11 @@
-using System.Diagnostics;
-
-namespace PixiEditor.Models.Commands.Templates;
+namespace PixiEditor.Models.Commands.Templates;
 
 internal partial class ShortcutProvider
 {
     public string Name { get; set; }
     
     public string LogoPath { get; set; }
+    public string HoverLogoPath { get; set; }
 
     /// <summary>
     /// Set this to true if this provider has default shortcuts

+ 10 - 0
src/PixiEditor/PixiEditor.csproj

@@ -335,6 +335,8 @@
 		<Resource Include="Images\Tools\LassoImage.png" />
 		<None Remove="Images\TemplateLogos\Aseprite.png" />
 		<Resource Include="Images\TemplateLogos\Aseprite.png" />
+		<None Remove="Images\TemplateLogos\Aseprite-Hover.png" />
+		<Resource Include="Images\TemplateLogos\Aseprite-Hover.png" />
 	</ItemGroup>
 	<ItemGroup>
 		<None Include="..\LICENSE">
@@ -374,4 +376,12 @@
 	    <XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
 	  </Page>
 	</ItemGroup>
+  <ItemGroup>
+    <Content Include="Data\*">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
+    <Content Include="Data\ShortcutActionMaps\*">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
 </Project>

+ 12 - 2
src/PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -92,12 +92,22 @@ internal class SettingsWindowViewModel : ViewModelBase
         dialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
         if (dialog.ShowDialog().GetValueOrDefault())
         {
-            var shortcuts = ShortcutFile.LoadTemplate(dialog.FileName)?.Shortcuts.ToList();
-            if (shortcuts is null)
+            List<Shortcut> shortcuts = new List<Shortcut>();
+            try
+            {
+                shortcuts = ShortcutFile.LoadTemplate(dialog.FileName)?.Shortcuts.ToList();
+                if (shortcuts is null)
+                {
+                    NoticeDialog.Show("Shortcuts file was not in a valid format", "Invalid file");
+                    return;
+                }
+            }
+            catch (Exception e)
             {
                 NoticeDialog.Show("Shortcuts file was not in a valid format", "Invalid file");
                 return;
             }
+            
             CommandController.Current.ResetShortcuts();
             CommandController.Current.Import(shortcuts, false);
             File.Copy(dialog.FileName, CommandController.ShortcutsPath, true);

+ 44 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -1,6 +1,7 @@
 using System.Diagnostics;
 using System.IO;
 using System.Reflection;
+using Microsoft.Win32;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes;
 using PixiEditor.Models.Commands.Attributes.Commands;
@@ -46,6 +47,49 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
 
         ProcessHelpers.ShellExecuteEV(path);
     }
+    
+    [Command.Debug("PixiEditor.Debug.DumpAllCommands", "Dump All Commands", "Dumps All Commands to a text file")]
+    public void DumpAllCommands()
+    {
+        SaveFileDialog dialog = new SaveFileDialog();
+        var dialogResult = dialog.ShowDialog();
+        if (dialogResult.HasValue && dialogResult.Value)
+        {
+            var commands = Owner.CommandController.Commands;
+
+            using StreamWriter writer = new StreamWriter(dialog.FileName);
+            foreach (var command in commands)
+            {
+                writer.WriteLine($"InternalName: {command.InternalName}");
+                writer.WriteLine($"Default Shortcut: {command.DefaultShortcut}");
+                writer.WriteLine($"IsDebug: {command.IsDebug}");
+                writer.WriteLine();
+            }
+        }
+    }
+    
+    [Command.Debug("PixiEditor.Debug.GenerateKeysTemplate", "Generate key bindings template", "Generates key bindings json template")]
+    public void GenerateKeysTemplate()
+    {
+        SaveFileDialog dialog = new SaveFileDialog();
+        var dialogResult = dialog.ShowDialog();
+        if (dialogResult.HasValue && dialogResult.Value)
+        {
+            var commands = Owner.CommandController.Commands;
+
+            using StreamWriter writer = new StreamWriter(dialog.FileName);
+            writer.WriteLine("{");
+            foreach (var command in commands)
+            {
+                if(command.IsDebug) continue;
+                writer.WriteLine($"\"\" : \"{command.InternalName}\",");
+            }
+            
+            writer.WriteLine("}");
+            
+            ProcessHelpers.ShellExecuteEV(dialog.FileName);
+        }
+    }
 
     [Command.Debug("PixiEditor.Debug.OpenInstallDirectory", "Open Installation Directory", "Open Installation Directory")]
     public static void OpenInstallLocation()

+ 13 - 2
src/PixiEditor/Views/Dialogs/ImportShortcutTemplatePopup.xaml

@@ -6,6 +6,7 @@
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:diag="clr-namespace:PixiEditor.Views.Dialogs"
         xmlns:xaml="clr-namespace:PixiEditor.Models.Commands.XAML"
+        xmlns:userControls="clr-namespace:PixiEditor.Views.UserControls"
         mc:Ignorable="d"
         Title="Import from template" Foreground="White"
         MinWidth="580"
@@ -32,9 +33,19 @@
                              TitleText="Import from template" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}"/>
         <ItemsControl Grid.Row="1" ItemsSource="{Binding Templates, ElementName=window}"
                       Margin="10,10,10,5">
+            <ItemsControl.ItemsPanel>
+                <ItemsPanelTemplate>
+                    <StackPanel Orientation="Horizontal"/>
+                </ItemsPanelTemplate>
+            </ItemsControl.ItemsPanel>
             <ItemsControl.ItemTemplate>
                 <DataTemplate>
-                    <Grid Margin="0,5,0,0">
+                    <userControls:ShortcutsTemplateCard 
+                        TemplateName="{Binding Name}" Margin="0 0 5 0" 
+                        Logo="{Binding LogoPath}" Cursor="Hand" 
+                        MouseLeftButtonUp="OnTemplateCardLeftMouseButtonDown"
+                        HoverLogo="{Binding Path=HoverLogoPath}"/>
+                    <!--<Grid Margin="0,5,0,0">
                         <Grid.ColumnDefinitions>
                             <ColumnDefinition Width="*"/>
                             <ColumnDefinition Width="Auto"/>
@@ -63,7 +74,7 @@
                                     CommandParameter="{Binding}" Content="Import defaults"
                                     Visibility="{Binding HasDefaultShortcuts, Converter={BoolToVisibilityConverter}}"/>
                         </StackPanel>
-                    </Grid>
+                    </Grid>-->
                 </DataTemplate>
             </ItemsControl.ItemTemplate>
         </ItemsControl>

+ 21 - 31
src/PixiEditor/Views/Dialogs/ImportShortcutTemplatePopup.xaml.cs

@@ -6,6 +6,7 @@ using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Templates;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Views.UserControls;
 
 namespace PixiEditor.Views.Dialogs;
 
@@ -37,37 +38,6 @@ internal partial class ImportShortcutTemplatePopup : Window
         Success(provider);
     }
 
-    [Command.Internal("PixiEditor.Shortcuts.Provider.ImportFile")]
-    public static void ImportFile(ShortcutProvider provider)
-    {
-        if (provider is not IShortcutFile defaults)
-        {
-            throw new ArgumentException("provider must implement IShortcutFile", nameof(provider));
-        }
-
-        var picker = new OpenFileDialog();
-
-        if (!picker.ShowDialog().GetValueOrDefault())
-        {
-            return;
-        }
-
-        try
-        {
-            var template = defaults.GetShortcutsTemplate(picker.FileName);
-
-            CommandController.Current.ResetShortcuts();
-            CommandController.Current.Import(template.Shortcuts);
-        }
-        catch (FileFormatException)
-        {
-            NoticeDialog.Show($"The file was not in a correct format", "Error");
-            return;
-        }
-
-        Success(provider);
-    }
-
     [Command.Internal("PixiEditor.Shortcuts.Provider.ImportInstallation")]
     public static void ImportInstallation(ShortcutProvider provider)
     {
@@ -102,4 +72,24 @@ internal partial class ImportShortcutTemplatePopup : Window
     {
         SystemCommands.CloseWindow(this);
     }
+
+    private void OnTemplateCardLeftMouseButtonDown(object sender, MouseButtonEventArgs e)
+    {
+        ShortcutsTemplateCard card = (ShortcutsTemplateCard)sender;
+        ShortcutProvider provider = card.DataContext as ShortcutProvider;
+        
+        ImportFromProvider(provider);
+    }
+
+    private void ImportFromProvider(ShortcutProvider provider)
+    {
+        if (provider.ProvidesFromInstallation && provider.HasInstallationPresent)
+        {
+            ImportInstallation(provider);
+        }
+        else if (provider.HasDefaultShortcuts)
+        {
+            ImportDefaults(provider);
+        }
+    }
 }

+ 32 - 5
src/PixiEditor/Views/UserControls/ShortcutsTemplateCard.xaml

@@ -5,12 +5,39 @@
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:local="clr-namespace:PixiEditor.Views.UserControls"
              mc:Ignorable="d" Background="Transparent"
-             d:DesignHeight="300" d:DesignWidth="300" Name="card">
-    <Border BorderThickness="1" Background="{StaticResource AccentColor}" CornerRadius="15">
+             d:DesignHeight="150" d:DesignWidth="150" Name="card">
+    <Border BorderThickness="1" Height="150" Width="150" Background="{StaticResource MainColor}" 
+            CornerRadius="15" MouseEnter="OnBorderMouseEnter" MouseLeave="BorderMouseLeave">
+        <Border.Triggers>
+            <EventTrigger RoutedEvent="Border.MouseEnter">
+                <BeginStoryboard>
+                    <Storyboard>
+                        <DoubleAnimation Storyboard.TargetName="img" Storyboard.TargetProperty="Width" 
+                                         From="72" To="100" Duration="0:0:0.15"/>
+                        <DoubleAnimation Storyboard.TargetName="img" Storyboard.TargetProperty="Height" 
+                                         From="72" To="100" Duration="0:0:0.15" />
+                    </Storyboard>
+                </BeginStoryboard>
+            </EventTrigger>
+            <EventTrigger RoutedEvent="Border.MouseLeave">
+                <BeginStoryboard>
+                    <Storyboard>
+                        <DoubleAnimation Storyboard.TargetName="img" Storyboard.TargetProperty="Width" 
+                                         From="100" To="72" Duration="0:0:0.15"/>
+                        <DoubleAnimation Storyboard.TargetName="img" Storyboard.TargetProperty="Height" 
+                                         From="100" To="72" Duration="0:0:0.15" />
+                    </Storyboard>
+                </BeginStoryboard>
+            </EventTrigger>
+        </Border.Triggers>
         <Grid>
-            <Image Source="{Binding ElementName=card, Path=ShortcutTemplate.Logo}"/>
-            <Label Style="{StaticResource BaseLabel}" Target="card" 
-                   Content="{Binding ElementName=card, Path=ShortcutTemplate.Name}"/>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="*"/>
+                <RowDefinition Height="30"/>
+            </Grid.RowDefinitions>
+            <Image Grid.Row="0" Grid.RowSpan="2" Name="img" HorizontalAlignment="Center" VerticalAlignment="Center" Height="72" Width="72" Source="{Binding ElementName=card, Path=Logo}"/>
+            <Label Grid.Row="1" HorizontalAlignment="Center" FontWeight="Bold" Margin="0" Padding="0" Style="{StaticResource BaseLabel}"
+                   Content="{Binding ElementName=card, Path=TemplateName}"/>
         </Grid>
     </Border>
 </UserControl>

+ 49 - 0
src/PixiEditor/Views/UserControls/ShortcutsTemplateCard.xaml.cs

@@ -1,13 +1,62 @@
 using System.Windows;
 using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media.Imaging;
+using PixiEditor.Models.Commands.Templates;
 
 namespace PixiEditor.Views.UserControls;
 
 public partial class ShortcutsTemplateCard : UserControl
 {
+    public static readonly DependencyProperty TemplateNameProperty = DependencyProperty.Register(
+        nameof(TemplateName), typeof(string), typeof(ShortcutsTemplateCard), new PropertyMetadata(default(string)));
+
+    public string TemplateName
+    {
+        get { return (string)GetValue(TemplateNameProperty); }
+        set { SetValue(TemplateNameProperty, value); }
+    }
+
+    public static readonly DependencyProperty LogoProperty = DependencyProperty.Register(
+        nameof(Logo), typeof(string), typeof(ShortcutsTemplateCard), new PropertyMetadata(default(string)));
+
+    public static readonly DependencyProperty HoverLogoProperty = DependencyProperty.Register(
+        nameof(HoverLogo), typeof(string), typeof(ShortcutsTemplateCard), new PropertyMetadata(default(string)));
+
+    public string HoverLogo
+    {
+        get { return (string)GetValue(HoverLogoProperty); }
+        set { SetValue(HoverLogoProperty, value); }
+    }
+    
+    public string Logo
+    {
+        get { return (string)GetValue(LogoProperty); }
+        set { SetValue(LogoProperty, value); }
+    }
     public ShortcutsTemplateCard()
     {
         InitializeComponent();
     }
+
+    private void OnBorderMouseEnter(object sender, MouseEventArgs e)
+    {
+        if (string.IsNullOrEmpty(HoverLogo))
+        {
+            return;
+        }
+        
+        img.Source = new BitmapImage(new Uri(HoverLogo, UriKind.Relative));
+    }
+
+    private void BorderMouseLeave(object sender, MouseEventArgs e)
+    {
+        if (string.IsNullOrEmpty(HoverLogo))
+        {
+            return;
+        }
+        
+        img.Source = new BitmapImage(new Uri(Logo, UriKind.Relative));
+    }
 }