Browse Source

Merge branch 'master' into fix/try-catch-update

Krzysztof Krysiński 1 week ago
parent
commit
e8e6e62167

+ 28 - 19
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -8,6 +8,7 @@ using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace ChunkyImageLib.Operations;
+
 internal class EllipseOperation : IMirroredDrawOperation
 {
     public bool IgnoreEmptyChunks => false;
@@ -21,14 +22,15 @@ internal class EllipseOperation : IMirroredDrawOperation
     private bool init = false;
     private VectorPath? outerPath;
     private VectorPath? innerPath;
-    
+
     private VectorPath? ellipseOutline;
     private VecF[]? ellipse;
     private VecF[]? ellipseFill;
     private RectI? ellipseFillRect;
     private bool antialiased;
 
-    public EllipseOperation(RectD location, Paintable strokePaintable, Paintable fillPaintable, float strokeWidth, double rotationRad,
+    public EllipseOperation(RectD location, Paintable strokePaintable, Paintable fillPaintable, float strokeWidth,
+        double rotationRad,
         bool antiAliased, Paint? paint = null)
     {
         this.location = location;
@@ -92,7 +94,7 @@ internal class EllipseOperation : IMirroredDrawOperation
 
         if (antialiased)
         {
-            DrawAntiAliased(surf);   
+            DrawAntiAliased(surf);
         }
         else
         {
@@ -109,22 +111,26 @@ internal class EllipseOperation : IMirroredDrawOperation
         {
             if (Math.Abs(rotation) < 0.001 && strokeWidth > 0)
             {
+                RectD rect = (RectD)ellipseFillRect!.Value;
+                fillPaintable.Bounds = location;
                 if (fillPaintable.AnythingVisible || paint.BlendMode != BlendMode.SrcOver)
                 {
                     paint.SetPaintable(fillPaintable);
                     surf.Canvas.DrawPoints(PointMode.Lines, ellipseFill!, paint);
-                    surf.Canvas.DrawRect((RectD)ellipseFillRect!.Value, paint);
+                    surf.Canvas.DrawRect(rect, paint);
                 }
-                
+
                 paint.SetPaintable(strokeWidth <= 0 ? fillPaintable : strokePaintable);
                 paint.StrokeWidth = 1f;
                 surf.Canvas.DrawPoints(PointMode.Points, ellipse!, paint);
+
+                fillPaintable.Bounds = null;
             }
             else
             {
                 surf.Canvas.Save();
                 surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
-                
+
                 if (fillPaintable.AnythingVisible || paint.BlendMode != BlendMode.SrcOver)
                 {
                     paint.SetPaintable(fillPaintable);
@@ -151,14 +157,15 @@ internal class EllipseOperation : IMirroredDrawOperation
                 surf.Canvas.Save();
                 surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
                 surf.Canvas.ClipPath(innerPath!);
-                surf.Canvas.DrawPaintable(fillPaintable, paint.BlendMode);
+                surf.Canvas.DrawPaintable(fillPaintable, paint.BlendMode, location);
                 surf.Canvas.Restore();
             }
+
             surf.Canvas.Save();
             surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
             surf.Canvas.ClipPath(outerPath!);
             surf.Canvas.ClipPath(innerPath!, ClipOperation.Difference);
-            surf.Canvas.DrawPaintable(strokePaintable, paint.BlendMode);
+            surf.Canvas.DrawPaintable(strokePaintable, paint.BlendMode, location);
             surf.Canvas.Restore();
         }
     }
@@ -167,24 +174,24 @@ internal class EllipseOperation : IMirroredDrawOperation
     {
         surf.Canvas.Save();
         surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
-        
+
         paint.IsAntiAliased = false;
         paint.SetPaintable(fillPaintable);
         paint.Style = PaintStyle.Fill;
-        
+
         RectD fillRect = ((RectD)location).Inflate(-strokeWidth / 2f);
-        
+
         surf.Canvas.DrawOval(fillRect.Center, fillRect.Size / 2f, paint);
 
         paint.IsAntiAliased = true;
         paint.SetPaintable(strokeWidth <= 0 ? fillPaintable : strokePaintable);
         paint.Style = PaintStyle.Stroke;
         paint.StrokeWidth = strokeWidth <= 0 ? 1f : strokeWidth;
-        
+
         RectD strokeRect = ((RectD)location).Inflate((-strokeWidth / 2f));
-        
+
         surf.Canvas.DrawOval(strokeRect.Center, strokeRect.Size / 2f, paint);
-        
+
         surf.Canvas.Restore();
     }
 
@@ -193,14 +200,15 @@ internal class EllipseOperation : IMirroredDrawOperation
         ShapeCorners corners = new((RectD)location);
         corners = corners.AsRotated(rotation, (VecD)location.Center);
         RectI bounds = (RectI)corners.AABBBounds.RoundOutwards();
-        
+
         var chunks = OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
         if (!fillPaintable?.AnythingVisible ?? false)
         {
-             chunks.ExceptWith(OperationHelper.FindChunksFullyInsideEllipse
-                (location.Center, location.Width / 2.0 - strokeWidth * 2, location.Height / 2.0 - strokeWidth * 2, ChunkyImage.FullChunkSize, rotation));
+            chunks.ExceptWith(OperationHelper.FindChunksFullyInsideEllipse
+            (location.Center, location.Width / 2.0 - strokeWidth * 2, location.Height / 2.0 - strokeWidth * 2,
+                ChunkyImage.FullChunkSize, rotation));
         }
-        
+
         return new AffectedArea(chunks, bounds);
     }
 
@@ -226,7 +234,8 @@ internal class EllipseOperation : IMirroredDrawOperation
             ((IPositionPaintable)finalStrokePaintable).Position = newLocation.Center;
         }
 
-        return new EllipseOperation(newLocation, finalStrokePaintable, finalFillPaintable, strokeWidth, rotation, antialiased, paint);
+        return new EllipseOperation(newLocation, finalStrokePaintable, finalFillPaintable, strokeWidth, rotation,
+            antialiased, paint);
     }
 
     public void Dispose()

+ 20 - 2
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -1,7 +1,9 @@
 using ChunkyImageLib.DataHolders;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Utils;
 using Drawie.Numerics;
 
 namespace ChunkyImageLib.Operations;
@@ -70,7 +72,7 @@ internal class RectangleOperation : IMirroredDrawOperation
                 surf.Canvas.ClipRoundRect(innerRect, vecInnerRadius, ClipOperation.Intersect);
             }
 
-            surf.Canvas.DrawPaintable(Data.FillPaintable, Data.BlendMode);
+            surf.Canvas.DrawPaintable(Data.FillPaintable, Data.BlendMode, rect);
             surf.Canvas.RestoreToCount(saved);
         }
 
@@ -88,13 +90,23 @@ internal class RectangleOperation : IMirroredDrawOperation
             surf.Canvas.ClipRoundRect(innerRect, vecInnerRadius, ClipOperation.Difference);
         }
 
-        surf.Canvas.DrawPaintable(Data.Stroke, Data.BlendMode);
+        surf.Canvas.DrawPaintable(Data.Stroke, Data.BlendMode, rect);
     }
 
     private void DrawAntiAliased(DrawingSurface surf, RectD rect, double radius)
     {
         // shrink radius too so corners match inner curve
         // Draw fill first
+        if (Data.FillPaintable != null)
+        {
+            Data.FillPaintable.Bounds = rect;
+        }
+
+        if (Data.Stroke != null)
+        {
+            Data.Stroke.Bounds = rect;
+        }
+
         if (Data.FillPaintable.AnythingVisible)
         {
             int saved = surf.Canvas.Save();
@@ -168,6 +180,12 @@ internal class RectangleOperation : IMirroredDrawOperation
                     (float)innerRadius, (float)innerRadius, paint);
             }
 
+            if(Data.FillPaintable != null)
+                Data.FillPaintable.Bounds = null;
+
+            if(Data.Stroke != null)
+                Data.Stroke.Bounds = null;
+
             surf.Canvas.Restore();
         }
     }

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit adfaa90105229e3183e3049276af982f3e5b1b5d
+Subproject commit 9e5f6dc3ab03cd67fa157b0952e7bb510c465e8b

+ 3 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1142,5 +1142,7 @@
   "MITER_STROKE_JOIN": "Miter",
   "BEVEL_STROKE_JOIN": "Bevel",
   "TOGGLE_FULLSCREEN": "Toggle Fullscreen",
-  "TOGGLE_FULLSCREEN_DESCRIPTIVE": "Enter or Exit Fullscreen Mode"
+  "TOGGLE_FULLSCREEN_DESCRIPTIVE": "Enter or Exit Fullscreen Mode",
+  "SHORTCUTS_IMPORTED_WITH_ERRORS": "Importing shortcuts finished with errors.\nSome shortcuts were not recognized and have been skipped.",
+  "OPEN_ERROR_LOG": "Open error log"
 }

+ 6 - 0
src/PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -86,6 +86,12 @@ internal class SupportedFilesHelper
         string? localPath = file.TryGetLocalPath();
 
         string extension = Path.GetExtension(localPath ?? file.Name);
+
+        if (string.IsNullOrEmpty(extension))
+        {
+            return allSupportedExtensions.First(i => i.CanSave);
+        }
+
         return allSupportedExtensions.Single(i => i.CanSave && i.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
     }
 

+ 11 - 1
src/PixiEditor/Models/Commands/ShortcutsTemplate.cs

@@ -11,6 +11,9 @@ public sealed class ShortcutsTemplate
 {
     public List<Shortcut> Shortcuts { get; set; }
 
+    [NonSerialized]
+    public List<string> Errors = new List<string>();
+
     public ShortcutsTemplate()
     {
         Shortcuts = new List<Shortcut>();
@@ -23,7 +26,14 @@ public sealed class ShortcutsTemplate
         {
             foreach (string command in keyDefinition.Commands)
             {
-                template.Shortcuts.Add(new Shortcut(keyDefinition.DefaultShortcut.ToKeyCombination(), command));
+                try
+                {
+                    template.Shortcuts.Add(new Shortcut(keyDefinition.DefaultShortcut.ToKeyCombination(), command));
+                }
+                catch (ArgumentException) // Invalid key
+                {
+                    template.Errors.Add($"Error for command {command}: Invalid key '{keyDefinition.DefaultShortcut.key}' with modifiers '{string.Join(", ", keyDefinition.DefaultShortcut.modifiers ?? [])}'");
+                }
             }
         }
 

+ 3 - 0
src/PixiEditor/Models/Commands/Templates/Providers/Parsers/KeyParser.cs

@@ -101,6 +101,9 @@ public static class KeyParser
             case ")":
                 parsed = Key.D0;
                 return true;
+            case "|":
+                parsed = Key.Oem5;
+                return true;
             default:
                 parsed = Key.None;
                 return false;

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -283,6 +283,7 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
                 new SetLowDpiRendering_Action(selectedMember.Id, toolbar.ForceLowDpiRendering));
         }
 
+        document.TextOverlayHandler.Font = null; // Forces refreshing glyphs
         document.TextOverlayHandler.Font = constructedText.Font;
         document.TextOverlayHandler.Spacing = toolbar.Spacing;
     }

+ 7 - 1
src/PixiEditor/Models/IO/CustomDocumentFormats/FontDocumentBuilder.cs

@@ -14,7 +14,13 @@ internal class FontDocumentBuilder : IDocumentBuilder
 
     public void Build(DocumentViewModelBuilder builder, string path)
     {
-        Font font = Font.FromFontFamily(new FontFamilyName(new Uri(path), Path.GetFileNameWithoutExtension(path)));
+        Font? font = Font.FromFontFamily(new FontFamilyName(new Uri(path), Path.GetFileNameWithoutExtension(path)));
+
+        if (font is null)
+        {
+            throw new Exception("Failed to load font");
+        }
+
         font.Size = 12;
 
         List<char> glyphs = new();

+ 43 - 75
src/PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -41,12 +41,10 @@ internal class SettingsPage : ObservableObject
 internal partial class SettingsWindowViewModel : ViewModelBase
 {
     private string searchTerm;
-    
-    [ObservableProperty]
-    private int visibleGroups;
-    
-    [ObservableProperty]
-    private int currentPage;
+
+    [ObservableProperty] private int visibleGroups;
+
+    [ObservableProperty] private int currentPage;
 
     public bool ShowUpdateTab
     {
@@ -85,7 +83,8 @@ internal partial class SettingsWindowViewModel : ViewModelBase
     [Command.Internal("PixiEditor.Shortcuts.Reset")]
     public static async Task ResetCommand()
     {
-        await new OptionsDialog<string>("ARE_YOU_SURE", new LocalizedString("WARNING_RESET_SHORTCUTS_DEFAULT"), MainWindow.Current!)
+        await new OptionsDialog<string>("ARE_YOU_SURE", new LocalizedString("WARNING_RESET_SHORTCUTS_DEFAULT"),
+            MainWindow.Current!)
         {
             { new LocalizedString("YES"), x => CommandController.Current.ResetShortcuts() },
             new LocalizedString("CANCEL"),
@@ -112,31 +111,13 @@ internal partial class SettingsWindowViewModel : ViewModelBase
             SuggestedStartLocation = suggestedStartLocation,
             FileTypeChoices = new List<FilePickerFileType>()
             {
-                new FilePickerFileType("PixiShorts (*.pixisc)")
-                {
-                    Patterns = new List<string>
-                    {
-                        "*.pixisc"
-                    },
-                },
-                new FilePickerFileType("json (*.json)")
-                {
-                    Patterns = new List<string>
-                    {
-                        "*.json"
-                    },
-                },
-                new FilePickerFileType("All files (*.*)")
-                {
-                    Patterns = new List<string>
-                    {
-                        "*.*"
-                    },
-                },
+                new FilePickerFileType("PixiShorts (*.pixisc)") { Patterns = new List<string> { "*.pixisc" }, },
+                new FilePickerFileType("json (*.json)") { Patterns = new List<string> { "*.json" }, },
+                new FilePickerFileType("All files (*.*)") { Patterns = new List<string> { "*.*" }, },
             },
         });
-        
-        
+
+
         if (file is not null)
         {
             try
@@ -146,10 +127,11 @@ internal partial class SettingsWindowViewModel : ViewModelBase
             catch (Exception ex)
             {
                 string errMessageTrimmed = ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message;
-                NoticeDialog.Show(title: "ERROR", message: new LocalizedString("UNKNOWN_ERROR_SAVING").Value + $" {errMessageTrimmed}");
+                NoticeDialog.Show(title: "ERROR",
+                    message: new LocalizedString("UNKNOWN_ERROR_SAVING").Value + $" {errMessageTrimmed}");
             }
         }
-        
+
         // Sometimes, focus was brought back to the last edited shortcut
         // TODO: Keyboard.ClearFocus(); should be there but I can't find an equivalent from avalonia
     }
@@ -159,37 +141,20 @@ internal partial class SettingsWindowViewModel : ViewModelBase
     {
         List<FilePickerFileType> fileTypes = new List<FilePickerFileType>
         {
-            new("PixiShorts (*.pixisc)")
-            {
-                Patterns = new List<string>
-                {
-                    "*.pixisc"
-                },
-            },
-            new("json (*.json)")
-            {
-                Patterns = new List<string>
-                {
-                    "*.json"
-                },
-            },
+            new("PixiShorts (*.pixisc)") { Patterns = new List<string> { "*.pixisc" }, },
+            new("json (*.json)") { Patterns = new List<string> { "*.json" }, },
         };
-        
+
         customShortcutFormats ??= ShortcutProvider.GetProviders().OfType<ICustomShortcutFormat>().ToList();
         AddCustomParsersFormat(customShortcutFormats, fileTypes);
-        
-        fileTypes.Add(new FilePickerFileType("All files (*.*)")
-        {
-            Patterns = new List<string>
+
+        fileTypes.Add(new FilePickerFileType("All files (*.*)") { Patterns = new List<string> { "*.*" }, });
+
+        fileTypes.Insert(0,
+            new FilePickerFileType($"All Shortcut files {string.Join(",", fileTypes.SelectMany(a => a.Patterns))}")
             {
-                "*.*"
-            },
-        });
-        
-        fileTypes.Insert(0, new FilePickerFileType($"All Shortcut files {string.Join(",", fileTypes.SelectMany(a => a.Patterns))}")
-        {
-            Patterns = fileTypes.SelectMany(a => a.Patterns).ToList(),
-        });
+                Patterns = fileTypes.SelectMany(a => a.Patterns).ToList(),
+            });
 
         IStorageFolder? suggestedLocation = null;
         try
@@ -205,34 +170,35 @@ internal partial class SettingsWindowViewModel : ViewModelBase
 
         IReadOnlyList<IStorageFile> files = await MainWindow.Current!.StorageProvider.OpenFilePickerAsync(new()
         {
-            AllowMultiple = false,
-            SuggestedStartLocation = suggestedLocation,
-            FileTypeFilter = fileTypes,
+            AllowMultiple = false, SuggestedStartLocation = suggestedLocation, FileTypeFilter = fileTypes,
         });
-        
+
         if (files.Count > 0)
         {
             List<Shortcut> shortcuts = new List<Shortcut>();
-            if (!TryImport(files[0], ref shortcuts))
+            if (!TryImport(files[0], ref shortcuts, out var errors))
                 return;
-            
+
             CommandController.Current.ResetShortcuts();
             CommandController.Current.Import(shortcuts, false);
             File.Copy(files[0].Path.LocalPath, CommandController.ShortcutsPath, true);
-            NoticeDialog.Show("SHORTCUTS_IMPORTED_SUCCESS", "SUCCESS");
+            ImportShortcutTemplatePopup.DisplayImportSuccessInfo(null, errors);
         }
-        
+
         // Sometimes, focus was brought back to the last edited shortcut
         // TODO: Keyboard.ClearFocus(); should be there but I can't find an equivalent from avalonia
     }
 
-    private static bool TryImport(IStorageFile file, ref List<Shortcut> shortcuts)
+    private static bool TryImport(IStorageFile file, ref List<Shortcut> shortcuts, out List<string>? errors)
     {
+        errors = null;
         if (file.Name.EndsWith(".pixisc") || file.Name.EndsWith(".json"))
         {
             try
             {
-                shortcuts = ShortcutFile.LoadTemplate(file.Path.LocalPath)?.Shortcuts.ToList();
+                var template = ShortcutFile.LoadTemplate(file.Path.LocalPath);
+                errors = template.Errors;
+                shortcuts = template.Shortcuts.ToList();
             }
             catch (Exception)
             {
@@ -258,7 +224,9 @@ internal partial class SettingsWindowViewModel : ViewModelBase
 
             try
             {
-                shortcuts = provider.KeysParser.Parse(file.Path.LocalPath, false)?.Shortcuts.ToList();
+                var template = provider.KeysParser.Parse(file.Path.LocalPath, false);
+                shortcuts = template?.Shortcuts.ToList();
+                errors = template?.Errors;
             }
             catch (RecoverableException e)
             {
@@ -270,7 +238,8 @@ internal partial class SettingsWindowViewModel : ViewModelBase
         return true;
     }
 
-    private static void AddCustomParsersFormat(IList<ICustomShortcutFormat>? customFormats, List<FilePickerFileType> listToAddTo)
+    private static void AddCustomParsersFormat(IList<ICustomShortcutFormat>? customFormats,
+        List<FilePickerFileType> listToAddTo)
     {
         if (customFormats is null || customFormats.Count == 0)
             return;
@@ -355,6 +324,7 @@ internal partial class SettingsWindowViewModel : ViewModelBase
 
                 group.IsVisible = visibleCommands > 0;
             }
+
             return;
         }
 
@@ -381,8 +351,7 @@ internal partial class SettingsWindowViewModel : ViewModelBase
 
 internal partial class GroupSearchResult : ObservableObject
 {
-    [ObservableProperty]
-    private bool isVisible;
+    [ObservableProperty] private bool isVisible;
 
     public LocalizedString DisplayName { get; set; }
 
@@ -397,8 +366,7 @@ internal partial class GroupSearchResult : ObservableObject
 
 internal partial class CommandSearchResult : ObservableObject
 {
-    [ObservableProperty]
-    private bool isVisible;
+    [ObservableProperty] private bool isVisible;
 
     public Models.Commands.Commands.Command Command { get; set; }
 

+ 36 - 26
src/PixiEditor/Views/Main/DocumentPreview.axaml

@@ -11,11 +11,11 @@
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              Name="uc"
              x:Class="PixiEditor.Views.Main.DocumentPreview">
-     <Grid>
+    <Grid>
         <Grid.RowDefinitions>
-            <RowDefinition Height="*"/>
-            <RowDefinition Height="5"/>
-            <RowDefinition Height="Auto"/>
+            <RowDefinition Height="*" />
+            <RowDefinition Height="5" />
+            <RowDefinition Height="Auto" />
         </Grid.RowDefinitions>
 
         <Grid x:Name="imageGrid" RenderOptions.BitmapInterpolationMode="None"
@@ -28,78 +28,88 @@
                 x:Name="viewport"
                 RenderInDocSize="{Binding ElementName=highDpiButton, Path=IsChecked}"
                 Document="{Binding Document, ElementName=uc}"
-                Background="{Binding ActiveItem.Value, ElementName=backgroundButton}"/>
+                Background="{Binding ActiveItem.Value, ElementName=backgroundButton}" />
         </Grid>
 
         <Grid Grid.Row="1">
             <Grid.Background>
-                <SolidColorBrush Color="{Binding ColorCursorColor, ElementName=uc, FallbackValue=Black}"/>
+                <SolidColorBrush Color="{Binding ColorCursorColor, ElementName=uc, FallbackValue=Black}" />
             </Grid.Background>
         </Grid>
         <StackPanel Margin="10, 0, 0, 0" Grid.Row="2" Orientation="Horizontal" Height="30"
                     Background="{DynamicResource ThemeBackgroundBrush}">
             <StackPanel.Styles>
                 <Style Selector="TextBlock">
-                    <Setter Property="VerticalAlignment" Value="Center"/>
+                    <Setter Property="VerticalAlignment" Value="Center" />
                 </Style>
             </StackPanel.Styles>
 
-            <TextBlock Text="{Binding ColorCursorPosition.X, ElementName=uc, StringFormat='X: {0}'}"/>
-            <TextBlock Text="{Binding ColorCursorPosition.Y, ElementName=uc, StringFormat='Y: {0}'}"/>
+            <TextBlock>
+                <Run Text="{Binding ColorCursorPosition.X, ElementName=uc, StringFormat='X: {0}'}" />
+                <Run Text="{Binding ColorCursorPosition.Y, ElementName=uc, StringFormat='Y: {0}'}" />
+            </TextBlock>
 
             <TextBlock VerticalAlignment="Center" Margin="10, 0, 0, 0">
                 <TextBlock.Text>
                     <MultiBinding Converter="{converters:FormattedColorConverter}">
-                        <Binding Path="ColorCursorColor" ElementName="uc"/>
-                        <Binding Path="ActiveItem.Value" ElementName="formatButton"/>
+                        <Binding Path="ColorCursorColor" ElementName="uc" />
+                        <Binding Path="ActiveItem.Value" ElementName="formatButton" />
                     </MultiBinding>
                 </TextBlock.Text>
             </TextBlock>
         </StackPanel>
-        <Grid Grid.Row="2" HorizontalAlignment="Right" Margin="0,0,5,0" ui:RenderOptionsBindable.BitmapInterpolationMode="{Binding ElementName=backgroundButton, Path=ActiveItem.ScalingMode}">
+        <Grid Grid.Row="2" HorizontalAlignment="Right" Margin="0,0,5,0"
+              ui:RenderOptionsBindable.BitmapInterpolationMode="{Binding ElementName=backgroundButton, Path=ActiveItem.ScalingMode}">
             <StackPanel Spacing="5" Orientation="Horizontal">
                 <StackPanel.Styles>
                     <Style Selector="ToggleButton#highDpiButton">
-                        <Setter Property="Content" Value="{DynamicResource icon-circle}"/>
+                        <Setter Property="Content" Value="{DynamicResource icon-circle}" />
                     </Style>
                     <Style Selector="ToggleButton#highDpiButton:checked">
-                        <Setter Property="Content" Value="{DynamicResource icon-lowres-circle}"/>
-                        <Setter Property="Background" Value="Transparent"/>
-                        <Setter Property="BorderThickness" Value="0"/>
+                        <Setter Property="Content" Value="{DynamicResource icon-lowres-circle}" />
+                        <Setter Property="Background" Value="Transparent" />
+                        <Setter Property="BorderThickness" Value="0" />
                     </Style>
                 </StackPanel.Styles>
 
-                <ToggleButton x:Name="highDpiButton" Classes="pixi-icon" localization:Translator.TooltipKey="TOGGLE_HIGH_RES_PREVIEW"/>
+                <ToggleButton x:Name="highDpiButton" Classes="pixi-icon"
+                              localization:Translator.TooltipKey="TOGGLE_HIGH_RES_PREVIEW" />
                 <input:ListSwitchButton x:Name="formatButton" Height="20">
                     <input:ListSwitchButton.Items>
                         <input:SwitchItemObservableCollection>
-                            <input:SwitchItem Content="RGBA" Background="{DynamicResource ThemeControlMidBrush}" Value="RGBA"/>
-                            <input:SwitchItem Content="HEX" Background="{DynamicResource ThemeControlMidBrush}" Value="HEX"/>
+                            <input:SwitchItem Content="RGBA" Background="{DynamicResource ThemeControlMidBrush}"
+                                              Value="RGBA" />
+                            <input:SwitchItem Content="HEX" Background="{DynamicResource ThemeControlMidBrush}"
+                                              Value="HEX" />
                         </input:SwitchItemObservableCollection>
                     </input:ListSwitchButton.Items>
                 </input:ListSwitchButton>
-                <input:ListSwitchButton RenderOptions.BitmapInterpolationMode="None" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Width="25" Height="20" x:Name="backgroundButton">
+                <input:ListSwitchButton RenderOptions.BitmapInterpolationMode="None"
+                                        BorderBrush="{DynamicResource ThemeBorderMidBrush}" Width="25" Height="20"
+                                        x:Name="backgroundButton">
                     <input:ListSwitchButton.Items>
                         <input:SwitchItemObservableCollection>
                             <input:SwitchItem ScalingMode="None">
                                 <input:SwitchItem.Background>
-                                    <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile" DestinationRect="0, 0, 25 25"/>
+                                    <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile"
+                                                DestinationRect="0, 0, 25 25" />
                                 </input:SwitchItem.Background>
                                 <input:SwitchItem.Value>
-                                    <ImageBrush DestinationRect="0, 10, 10, 10" Source="/Images/CheckerTile.png" TileMode="Tile"/>
+                                    <ImageBrush DestinationRect="0, 10, 10, 10" Source="/Images/CheckerTile.png"
+                                                TileMode="Tile" />
                                 </input:SwitchItem.Value>
                             </input:SwitchItem>
                             <input:SwitchItem Value="Transparent">
                                 <input:SwitchItem.Background>
-                                    <ImageBrush Source="/Images/DiagonalRed.png"/>
+                                    <ImageBrush Source="/Images/DiagonalRed.png" />
                                 </input:SwitchItem.Background>
                             </input:SwitchItem>
-                            <input:SwitchItem Background="White" Value="White"/>
-                            <input:SwitchItem Background="Black" Value="Black"/>
+                            <input:SwitchItem Background="White" Value="White" />
+                            <input:SwitchItem Background="Black" Value="Black" />
                         </input:SwitchItemObservableCollection>
                     </input:ListSwitchButton.Items>
                 </input:ListSwitchButton>
             </StackPanel>
         </Grid>
     </Grid>
-</UserControl>
+</UserControl>

+ 44 - 5
src/PixiEditor/Views/Shortcuts/ImportShortcutTemplatePopup.axaml.cs

@@ -6,6 +6,8 @@ using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Templates;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.IO;
+using PixiEditor.OperatingSystem;
 using PixiEditor.UI.Common.Localization;
 using PixiEditor.Views.Dialogs;
 
@@ -38,7 +40,7 @@ internal partial class ImportShortcutTemplatePopup : PixiEditorPopup
         CommandController.Current.Import(defaults.DefaultShortcuts);
 
         if (!quiet)
-            Success(provider);
+            DisplayImportSuccessInfo(provider.Name, null);
     }
 
     [Command.Internal("PixiEditor.Shortcuts.Provider.ImportInstallation")]
@@ -55,10 +57,17 @@ internal partial class ImportShortcutTemplatePopup : PixiEditorPopup
         }
 
         CommandController.Current.ResetShortcuts();
+        List<string> errors = new List<string>();
 
         try
         {
-            CommandController.Current.Import(defaults.GetInstalledShortcuts().Shortcuts);
+            var template = defaults.GetInstalledShortcuts();
+            if (template?.Errors?.Count > 0)
+            {
+                errors.AddRange(template.Errors);
+            }
+
+            CommandController.Current.Import(template.Shortcuts);
         }
         catch (RecoverableException e)
         {
@@ -68,12 +77,42 @@ internal partial class ImportShortcutTemplatePopup : PixiEditorPopup
 
         if (!quiet)
         {
-            Success(provider);
+            DisplayImportSuccessInfo(provider.Name, errors);
         }
     }
 
-    private static void Success(ShortcutProvider provider) =>
-        NoticeDialog.Show(new LocalizedString("SHORTCUTS_IMPORTED", provider.Name), "SUCCESS");
+    public static void DisplayImportSuccessInfo(string? providerName, List<string>? errors)
+    {
+        string title = providerName != null ? "SHORTCUTS_IMPORTED" : "SHORTCUTS_IMPORTED_SUCCESS";
+        if (errors == null || errors.Count == 0)
+        {
+            NoticeDialog.Show(new LocalizedString(title, providerName), "SUCCESS");
+        }
+        else
+        {
+            string errorMessage = string.Join("\n", errors);
+            string errorLogPath = Path.Combine(Paths.TempFilesPath, "shortcut_import_errors.txt");
+            try
+            {
+                File.WriteAllText(errorLogPath, errorMessage);
+            }
+            catch
+            {
+                // ignored
+            }
+
+            OptionsDialog<string> dialog = new(
+                "WARNING",
+                new LocalizedString("SHORTCUTS_IMPORTED_WITH_ERRORS", providerName),
+                MainWindow.Current!)
+            {
+                { new LocalizedString("OPEN_ERROR_LOG"), x => { IOperatingSystem.Current.OpenUri(errorLogPath); } },
+                { new LocalizedString("CLOSE"), x => { } }
+            };
+
+            dialog.ShowDialog();
+        }
+    }
 
     // TODO figure out what these are for
     /*