Răsfoiți Sursa

Merge branch 'master' of https://github.com/PixiEditor/PixiEditor

Krzysztof Krysiński 2 ani în urmă
părinte
comite
a3b9da3707

+ 11 - 0
src/ChunkyImageLib/ChunkyImage.cs

@@ -606,6 +606,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         }
     }
 
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public void EnqueueDrawPixel(VecI pos, PixelProcessor pixelProcessor, BlendMode blendMode)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            PixelOperation operation = new(pos, pixelProcessor, blendMode);
+            EnqueueOperation(operation);
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
     {

+ 29 - 2
src/ChunkyImageLib/Operations/PixelOperation.cs

@@ -6,6 +6,7 @@ using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
 namespace ChunkyImageLib.Operations;
 
+public delegate Color PixelProcessor(Color input);
 internal class PixelOperation : IMirroredDrawOperation
 {
     public bool IgnoreEmptyChunks => false;
@@ -14,6 +15,8 @@ internal class PixelOperation : IMirroredDrawOperation
     private readonly BlendMode blendMode;
     private readonly Paint paint;
 
+    private readonly PixelProcessor? _colorProcessor = null;
+
     public PixelOperation(VecI pixel, Color color, BlendMode blendMode)
     {
         this.pixel = pixel;
@@ -22,10 +25,18 @@ internal class PixelOperation : IMirroredDrawOperation
         paint = new Paint() { BlendMode = blendMode };
     }
 
+    public PixelOperation(VecI pixel, PixelProcessor colorProcessor, BlendMode blendMode)
+    {
+        this.pixel = pixel;
+        this._colorProcessor = colorProcessor;
+        this.blendMode = blendMode;
+        paint = new Paint() { BlendMode = blendMode };
+    }
+
     public void DrawOnChunk(Chunk chunk, VecI chunkPos)
     {
         // a hacky way to make the lines look slightly better on non full res chunks
-        paint.Color = new Color(color.R, color.G, color.B, (byte)(color.A * chunk.Resolution.Multiplier()));
+        paint.Color = GetColor(chunk, chunkPos);
 
         DrawingSurface surf = chunk.Surface.DrawingSurface;
         surf.Canvas.Save();
@@ -35,6 +46,17 @@ internal class PixelOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
     }
 
+    private Color GetColor(Chunk chunk, VecI chunkPos)
+    {
+        Color pixelColor = color;
+        if (_colorProcessor != null)
+        {
+            pixelColor = _colorProcessor(chunk.Surface.GetSRGBPixel(pixel - chunkPos * ChunkyImage.FullChunkSize));
+        }
+
+        return new Color(pixelColor.R, pixelColor.G, pixelColor.B, (byte)(pixelColor.A * chunk.Resolution.Multiplier()));
+    }
+
     public AffectedArea FindAffectedArea(VecI imageSize)
     {
         return new AffectedArea(new HashSet<VecI>() { OperationHelper.GetChunkPos(pixel, ChunkyImage.FullChunkSize) }, new RectI(pixel, VecI.One));
@@ -46,7 +68,12 @@ internal class PixelOperation : IMirroredDrawOperation
         if (verAxisX is not null)
             pixelRect = (RectI)pixelRect.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
-            pixelRect = (RectI)pixelRect.ReflectY((double)horAxisY).Round();
+            pixelRect = (RectI)pixelRect.ReflectY((double)horAxisY);
+        if (_colorProcessor != null)
+        {
+            return new PixelOperation(pixelRect.Pos, _colorProcessor, blendMode);
+        }
+
         return new PixelOperation(pixelRect.Pos, color, blendMode);
     }
 

+ 1 - 3
src/ChunkyImageLib/Surface.cs

@@ -22,9 +22,7 @@ public class Surface : IDisposable
     public Surface(VecI size)
     {
         if (size.X < 1 || size.Y < 1)
-            throw new ArgumentException("Width and height must be >1");
-        if (size.X > 10000 || size.Y > 10000)
-            throw new ArgumentException("Width and height must be <=10000");
+            throw new ArgumentException("Width and height must be >=1");
 
         Size = size;
 

+ 8 - 3
src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs

@@ -88,9 +88,14 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
             
             for (VecI pos = new VecI(left.X, y); pos.X <= right.X; pos.X++)
             {
-                Color pixel = tempSurface.GetSRGBPixel(pos);
-                Color newColor = ColorHelper.ChangeColorBrightness(pixel, correctionFactor);
-                layerImage.EnqueueDrawPixel(pos + offset, newColor, BlendMode.Src);
+                layerImage.EnqueueDrawPixel(
+                    pos + offset,
+                    (pixel) =>
+                    {
+                        Color newColor = ColorHelper.ChangeColorBrightness(pixel, correctionFactor);
+                        return ColorHelper.ChangeColorBrightness(newColor, correctionFactor);
+                    },
+                    BlendMode.Src);
             }
         }
     }

+ 10 - 13
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWand_Change.cs

@@ -10,19 +10,15 @@ internal class MagicWand_Change : Change
     private VectorPath? originalPath;
     private VectorPath path = new() { FillType = PathFillType.EvenOdd };
     private VecI point;
-    private readonly Guid memberGuid;
-    private readonly bool referenceAll;
-    private readonly bool drawOnMask;
+    private readonly List<Guid> memberGuids;
     private readonly SelectionMode mode;
 
     [GenerateMakeChangeAction]
-    public MagicWand_Change(Guid memberGuid, VecI point, SelectionMode mode, bool referenceAll, bool drawOnMask)
+    public MagicWand_Change(List<Guid> memberGuids, VecI point, SelectionMode mode)
     {
         path.MoveTo(point);
         this.mode = mode;
-        this.memberGuid = memberGuid;
-        this.referenceAll = referenceAll;
-        this.drawOnMask = drawOnMask;
+        this.memberGuids = memberGuids;
         this.point = point;
     }
 
@@ -34,13 +30,14 @@ internal class MagicWand_Change : Change
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
-        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-
         HashSet<Guid> membersToReference = new();
-        if (referenceAll)
-            target.ForEveryReadonlyMember(member => membersToReference.Add(member.GuidValue));
-        else
-            membersToReference.Add(memberGuid);
+
+        target.ForEveryReadonlyMember(member =>
+        {
+            if (memberGuids.Contains(member.GuidValue))
+                membersToReference.Add(member.GuidValue);
+        });
+
         path = MagicWandHelper.DoMagicWandFloodFill(point, membersToReference, target);
 
         ignoreInUndo = false;

+ 34 - 0
src/PixiEditor/Helpers/CrashHelper.cs

@@ -1,4 +1,7 @@
 using System.Globalization;
+using System.IO;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
 using System.Text;
 using ByteSizeLib;
 using Hardware.Info;
@@ -97,4 +100,35 @@ internal class CrashHelper
             }
         }
     }
+
+    public static async Task SendExceptionInfoToWebhook(Exception e, [CallerFilePath] string filePath = "<unknown>", [CallerMemberName] string memberName = "<unknown>")
+    {
+        if (ViewModelMain.Current.DebugSubViewModel.IsDebugBuild)
+            return;
+        await SendReportTextToWebhook(CrashReport.Generate(e), $"{filePath}; Method {memberName}");
+    }
+
+    public static async Task SendReportTextToWebhook(CrashReport report, string catchLocation = null)
+    {
+        string reportText = report.ReportText;
+        if (catchLocation is not null)
+        {
+            reportText = $"The report was generated from an exception caught in {catchLocation}.\r\n{reportText}";
+        }
+
+        byte[] bytes = Encoding.UTF8.GetBytes(reportText);
+        string filename = Path.GetFileNameWithoutExtension(report.FilePath) + ".txt";
+
+        MultipartFormDataContent formData = new MultipartFormDataContent
+        {
+            { new ByteArrayContent(bytes, 0, bytes.Length), "crash-report", filename }
+        };
+        try
+        {
+            using HttpClient httpClient = new HttpClient();
+            string url = BuildConstants.CrashReportWebhookUrl;
+            await httpClient.PostAsync(url, formData);
+        }
+        catch { }
+    }
 }

+ 19 - 25
src/PixiEditor/Helpers/VersionHelpers.cs

@@ -10,37 +10,31 @@ internal static class VersionHelpers
 
     public static string GetCurrentAssemblyVersion(Func<Version, string> toString) => toString(GetCurrentAssemblyVersion());
 
-    public static string GetCurrentAssemblyVersionString()
+    public static string GetCurrentAssemblyVersionString(bool moreSpecific = false)
     {
         StringBuilder builder = new(GetCurrentAssemblyVersion().ToString());
 
-        bool isDone = false;
-
-        AppendDevBuild(builder, ref isDone);
+#if DEVRELEASE
+        builder.Append(" Dev Build");
+        return builder.ToString();
+#elif MSIX_DEBUG
+        builder.Append(" MSIX Debug Build");
+        return builder.ToString();
+#elif DEBUG
+        builder.Append(" Debug Build");
+        return builder.ToString();
+#endif
 
-        if (isDone)
-        {
+        if (!moreSpecific)
             return builder.ToString();
-        }
-
-        AppendDebugBuild(builder, ref isDone);
 
+#if STEAM
+        builder.Append(" Steam Build");
+#elif MSIX
+        builder.Append(" MSIX Build");
+#elif RELEASE
+        builder.Append(" Release Build");
+#endif
         return builder.ToString();
     }
-
-    [Conditional("DEVRELEASE")]
-    private static void AppendDevBuild(StringBuilder builder, ref bool done)
-    {
-        done = true;
-
-        builder.Append(" Dev Build");
-    }
-
-    [Conditional("DEBUG")]
-    private static void AppendDebugBuild(StringBuilder builder, ref bool done)
-    {
-        done = true;
-
-        builder.Append(" Debug Build");
-    }
 }

+ 19 - 3
src/PixiEditor/Models/Commands/CommandController.cs

@@ -282,15 +282,31 @@ internal class CommandController
 
             var parameters = method?.GetParameters();
 
-            Action<object> action;
+            async void ActionOnException(Task faultedTask)
+            {
+                // since this method is "async void" and not "async Task", the runtime will propagate exceptions out if it
+                // (instead of putting them into the returned task and forgetting about them)
+                await faultedTask; // this instantly throws the exception from the already faulted task
+            }
 
+            Action<object> action;
             if (parameters is not { Length: 1 })
             {
-                action = x => method.Invoke(instance, null);
+                action = x =>
+                {
+                    object result = method.Invoke(instance, null);
+                    if (result is Task task)
+                        task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
+                };
             }
             else
             {
-                action = x => method.Invoke(instance, new[] { x });
+                action = x =>
+                {
+                    object result = method.Invoke(instance, new[] { x });
+                    if (result is Task task)
+                        task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
+                };
             }
 
             string name = attribute.InternalName;

+ 1 - 1
src/PixiEditor/Models/DataHolders/CrashReport.cs

@@ -19,7 +19,7 @@ internal class CrashReport : IDisposable
         DateTime currentTime = DateTime.Now;
 
         builder
-            .AppendLine($"PixiEditor {VersionHelpers.GetCurrentAssemblyVersionString()} crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss}\n")
+            .AppendLine($"PixiEditor {VersionHelpers.GetCurrentAssemblyVersionString(moreSpecific: true)} crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss}\n")
             .AppendLine("-----System Information----")
             .AppendLine("General:")
             .AppendLine($"  OS: {Environment.OSVersion.VersionString}")

+ 7 - 10
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs

@@ -11,28 +11,25 @@ internal class MagicWandToolExecutor : UpdateableChangeExecutor
 {
     private bool considerAllLayers;
     private bool drawOnMask;
-    private Guid memberGuid;
+    private List<Guid> memberGuids;
     private SelectionMode mode;
 
     public override ExecutionState Start()
     {
         var magicWand = ViewModelMain.Current?.ToolsSubViewModel.GetTool<MagicWandToolViewModel>();
-        var member = document!.SelectedStructureMember;
+        var members = document!.ExtractSelectedLayers(true);
 
-        if (magicWand is null || member is null)
-            return ExecutionState.Error;
-        drawOnMask = member is not LayerViewModel layer || layer.ShouldDrawOnMask;
-        if (drawOnMask && !member.HasMaskBindable)
-            return ExecutionState.Error;
-        if (!drawOnMask && member is not LayerViewModel)
+        if (magicWand is null || members.Count == 0)
             return ExecutionState.Error;
 
         mode = magicWand.SelectMode;
-        memberGuid = member.GuidValue;
+        memberGuids = members;
         considerAllLayers = magicWand.DocumentScope == DocumentScope.AllLayers;
+        if (considerAllLayers)
+            memberGuids = document!.StructureHelper.GetAllLayers().Select(x => x.GuidValue).ToList();
         var pos = controller!.LastPixelPosition;
 
-        internals!.ActionAccumulator.AddActions(new MagicWand_Action(memberGuid, pos, mode, considerAllLayers, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new MagicWand_Action(memberGuids, pos, mode));
 
         return ExecutionState.Success;
     }

+ 23 - 10
src/PixiEditor/Models/IO/Exporter.cs

@@ -1,6 +1,8 @@
 using System.IO;
 using System.IO.Compression;
+using System.Reflection.Metadata;
 using System.Runtime.InteropServices;
+using System.Security;
 using System.Windows;
 using System.Windows.Media.Imaging;
 using ChunkyImageLib;
@@ -22,8 +24,10 @@ internal enum DialogSaveResult
     Success = 0,
     InvalidPath = 1,
     ConcurrencyError = 2,
-    UnknownError = 3,
-    Cancelled = 4,
+    SecurityError = 3,
+    IoError = 4,
+    UnknownError = 5,
+    Cancelled = 6,
 }
 
 internal enum SaveResult
@@ -31,7 +35,9 @@ internal enum SaveResult
     Success = 0,
     InvalidPath = 1,
     ConcurrencyError = 2,
-    UnknownError = 3,
+    SecurityError = 3,
+    IoError = 4,
+    UnknownError = 5,
 }
 
 internal class Exporter
@@ -97,9 +103,8 @@ internal class Exporter
             {
                 return SaveResult.UnknownError;
             }
-            
-            if (!TrySaveAs(encodersFactory[typeFromPath](), pathWithExtension, bitmap, exportSize))
-                return SaveResult.UnknownError;
+
+            return TrySaveAs(encodersFactory[typeFromPath](), pathWithExtension, bitmap, exportSize);
         }
         else
         {
@@ -149,7 +154,7 @@ internal class Exporter
     /// <summary>
     /// Saves image to PNG file. Messes with the passed bitmap.
     /// </summary>
-    private static bool TrySaveAs(BitmapEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
+    private static SaveResult TrySaveAs(BitmapEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
     {
         try
         {
@@ -163,10 +168,18 @@ internal class Exporter
             encoder.Frames.Add(BitmapFrame.Create(bitmap.ToWriteableBitmap()));
             encoder.Save(stream);
         }
-        catch (Exception err)
+        catch (SecurityException)
+        {
+            return SaveResult.SecurityError;
+        }
+        catch (IOException)
         {
-            return false;
+            return SaveResult.IoError;
         }
-        return true;
+        catch
+        {
+            return SaveResult.UnknownError;
+        }
+        return SaveResult.Success;
     }
 }

+ 1 - 19
src/PixiEditor/ViewModels/CrashReportViewModel.cs

@@ -40,25 +40,7 @@ internal class CrashReportViewModel : ViewModelBase
         AttachDebuggerCommand = new(AttachDebugger);
 
         if (!IsDebugBuild)
-            SendReportTextToWebhook(report);
-    }
-
-    private async void SendReportTextToWebhook(CrashReport report)
-    {
-        byte[] bytes = Encoding.UTF8.GetBytes(report.ReportText);
-        string filename = Path.GetFileNameWithoutExtension(report.FilePath) + ".txt";
-
-        MultipartFormDataContent formData = new MultipartFormDataContent
-        {
-            { new ByteArrayContent(bytes, 0, bytes.Length), "crash-report", filename }
-        };
-        try
-        {
-            using HttpClient httpClient = new HttpClient();
-            string url = BuildConstants.CrashReportWebhookUrl;
-            await httpClient.PostAsync(url, formData);
-        }
-        catch { }
+            _ = CrashHelper.SendReportTextToWebhook(report);
     }
 
     public void RecoverDocuments(object args)

+ 48 - 2
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -520,8 +520,54 @@ internal partial class DocumentViewModel : NotifyableObject
     /// </summary>
     public List<Guid> GetSelectedMembers()
     {
-        List<Guid> layerGuids = new List<Guid>() { SelectedStructureMember.GuidValue };
-        layerGuids.AddRange( SoftSelectedStructureMembers.Select(x => x.GuidValue));
+        List<Guid> layerGuids = new List<Guid>();
+        if (SelectedStructureMember is not null)
+            layerGuids.Add(SelectedStructureMember.GuidValue);
+
+        layerGuids.AddRange(SoftSelectedStructureMembers.Select(x => x.GuidValue));
         return layerGuids;
     }
+
+    public List<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false)
+    {
+        var result = new List<Guid>();
+        List<Guid> selectedMembers = GetSelectedMembers();
+        foreach (var member in selectedMembers)
+        {
+            var foundMember = StructureHelper.Find(member);
+            if (foundMember != null)
+            {
+                if (foundMember is LayerViewModel layer && selectedMembers.Contains(foundMember.GuidValue) && !result.Contains(layer.GuidValue))
+                {
+                    result.Add(layer.GuidValue);
+                }
+                else if (foundMember is FolderViewModel folder && selectedMembers.Contains(foundMember.GuidValue))
+                {
+                    if (includeFoldersWithMask && folder.HasMaskBindable && !result.Contains(folder.GuidValue))
+                        result.Add(folder.GuidValue);
+                    ExtractSelectedLayers(folder, result, includeFoldersWithMask);
+                }
+            }
+        }
+        return result;
+    }
+
+    private void ExtractSelectedLayers(FolderViewModel folder, List<Guid> list,
+        bool includeFoldersWithMask)
+    {
+        foreach (var member in folder.Children)
+        {
+            if (member is LayerViewModel layer && !list.Contains(layer.GuidValue))
+            {
+                list.Add(layer.GuidValue);
+            }
+            else if (member is FolderViewModel childFolder)
+            {
+                if (includeFoldersWithMask && childFolder.HasMaskBindable && !list.Contains(childFolder.GuidValue))
+                    list.Add(childFolder.GuidValue);
+
+                ExtractSelectedLayers(childFolder, list, includeFoldersWithMask);
+            }
+        }
+    }
 }

+ 25 - 8
src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -36,22 +36,39 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         UpdateDebugMode(preferences.GetPreference<bool>("IsDebugModeEnabled"));
     }
 
-    [Command.Debug("PixiEditor.Debug.OpenTempDirectory", @"%Temp%\PixiEditor", "Open Temp Directory", "Open Temp Directory", IconPath = "Folder.png")]
-    [Command.Debug("PixiEditor.Debug.OpenLocalAppDataDirectory", @"%LocalAppData%\PixiEditor", "Open Local AppData Directory", "Open Local AppData Directory", IconPath = "Folder.png")]
-    [Command.Debug("PixiEditor.Debug.OpenRoamingAppDataDirectory", @"%AppData%\PixiEditor", "Open Roaming AppData Directory", "Open Roaming AppData Directory", IconPath = "Folder.png")]
-    [Command.Debug("PixiEditor.Debug.OpenCrashReportsDirectory", @"%LocalAppData%\PixiEditor\crash_logs", "Open Crash Reports Directory", "Open Crash Reports Directory", IconPath = "Folder.png")]
     public static void OpenFolder(string path)
     {
-        string expandedPath = Environment.ExpandEnvironmentVariables(path);
-        if (!Directory.Exists(expandedPath))
+        if (!Directory.Exists(path))
         {
-            NoticeDialog.Show($"{expandedPath} does not exist.", "Location does not exist");
+            NoticeDialog.Show($"{path} does not exist.", "Location does not exist");
             return;
         }
 
         ProcessHelpers.ShellExecuteEV(path);
     }
-    
+
+    [Command.Debug("PixiEditor.Debug.OpenLocalAppDataDirectory", @"PixiEditor", "Open Local AppData Directory", "Open Local AppData Directory", IconPath = "Folder.png")]
+    [Command.Debug("PixiEditor.Debug.OpenCrashReportsDirectory", @"PixiEditor\crash_logs", "Open Crash Reports Directory", "Open Crash Reports Directory", IconPath = "Folder.png")]
+    public static void OpenLocalAppDataFolder(string subDirectory)
+    {
+        var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), subDirectory);
+        OpenFolder(path);
+    }
+
+    [Command.Debug("PixiEditor.Debug.OpenRoamingAppDataDirectory", @"PixiEditor", "Open Roaming AppData Directory", "Open Roaming AppData Directory", IconPath = "Folder.png")]
+    public static void OpenAppDataFolder(string subDirectory)
+    {
+        var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), subDirectory);
+        OpenFolder(path);
+    }
+
+    [Command.Debug("PixiEditor.Debug.OpenTempDirectory", @"PixiEditor", "Open Temp Directory", "Open Temp Directory", IconPath = "Folder.png")]
+    public static void OpenTempFolder(string subDirectory)
+    {
+        var path = Path.Combine(Path.GetTempPath(), subDirectory);
+        OpenFolder(path);
+    }
+
     [Command.Debug("PixiEditor.Debug.DumpAllCommands", "Dump All Commands", "Dump All Commands to a text file")]
     public void DumpAllCommands()
     {

+ 11 - 3
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -323,13 +323,19 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         switch (result)
         {
             case DialogSaveResult.InvalidPath:
-                NoticeDialog.Show("Error", "Couldn't save the file to the specified location");
+                NoticeDialog.Show(title: "Error", message: "Couldn't save the file to the specified location");
                 break;
             case DialogSaveResult.ConcurrencyError:
-                NoticeDialog.Show("Internal error", "An internal error occured while saving. Please try again.");
+                NoticeDialog.Show(title: "Internal error", message: "An internal error occured while saving. Please try again.");
+                break;
+            case DialogSaveResult.SecurityError:
+                NoticeDialog.Show(title: "Security error", message: "No rights to write to the specified location.");
+                break;
+            case DialogSaveResult.IoError:
+                NoticeDialog.Show(title: "IO error", message: "Error while writing to disk.");
                 break;
             case DialogSaveResult.UnknownError:
-                NoticeDialog.Show("Error", "An error occured while saving.");
+                NoticeDialog.Show(title: "Error", message: "An error occured while saving.");
                 break;
         }
     }
@@ -362,6 +368,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
         foreach (string path in paths)
         {
+            if (!File.Exists(path))
+                continue;
             documents.Add(new RecentlyOpenedDocument(path));
         }
 

+ 11 - 2
src/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs

@@ -1,6 +1,7 @@
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes;
 using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
@@ -19,8 +20,16 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Links.OpenRepository", "https://github.com/PixiEditor/PixiEditor", "Repository", "Open Repository", IconPath = "Globe.png")]
     [Command.Basic("PixiEditor.Links.OpenLicense", "https://github.com/PixiEditor/PixiEditor/blob/master/LICENSE", "License", "Open License", IconPath = "Globe.png")]
     [Command.Basic("PixiEditor.Links.OpenOtherLicenses", "https://pixieditor.net/docs/Third-party-licenses", "Third Party Licenses", "Open Third Party Licenses", IconPath = "Globe.png")]
-    public static void OpenHyperlink(string url)
+    public static async Task OpenHyperlink(string url)
     {
-        ProcessHelpers.ShellExecute(url);
+        try
+        {
+            ProcessHelpers.ShellExecute(url);
+        }
+        catch (Exception e)
+        {
+            NoticeDialog.Show(title: "Error", message: $"Couldn't open the address {url} in your default browser");
+            await CrashHelper.SendExceptionInfoToWebhook(e);
+        }
     }
 }