Browse Source

Merge pull request #590 from PixiEditor/pulled-out-fixes

Fixes
Krzysztof Krysiński 1 year ago
parent
commit
0fe3312578

+ 38 - 20
src/PixiEditor/App.xaml.cs

@@ -6,6 +6,7 @@ using System.Windows.Interop;
 using System.Windows.Media;
 using System.Windows.Threading;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Helpers;
 using PixiEditor.Models.AppExtensions;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Models.Controllers;
@@ -42,34 +43,37 @@ internal partial class App : Application
 
         if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
         {
-            CrashReport report = CrashReport.Parse(groups[2].Value);
-            MainWindow = new CrashReportDialog(report);
-            MainWindow.Show();
+            try
+            {
+                CrashReport report = CrashReport.Parse(groups[2].Value);
+                MainWindow = new CrashReportDialog(report);
+                MainWindow.Show();
+            }
+            catch (Exception exception)
+            {
+                try
+                {
+                    CrashHelper.SendExceptionInfoToWebhook(exception, true);
+                }
+                finally
+                {
+                    MessageBox.Show("Fatal error", $"Fatal error while trying to open crash report in App.OnStartup()\n{exception}");
+                }
+            }
+
             return;
         }
 
-        #if !STEAM
+#if !STEAM
         if (!HandleNewInstance())
         {
             return;
         }
-        #endif
-
-        LoadingWindow.ShowInNewThread();
-
-        AddNativeAssets();
-
-        InitPlatform();
-
-        ExtensionLoader extensionLoader = new ExtensionLoader();
-        extensionLoader.LoadExtensions();
-
+#endif
+        
+        var extensionLoader = InitApp();
+        
         MainWindow = new MainWindow(extensionLoader);
-        MainWindow.ContentRendered += (_, _) =>
-        {
-            LoadingWindow.Instance.SafeClose();
-            MainWindow.Activate();
-        };
         MainWindow.Show();
     }
 
@@ -80,6 +84,20 @@ internal partial class App : Application
         platform.PerformHandshake();
     }
 
+    public ExtensionLoader InitApp()
+    {
+        LoadingWindow.ShowInNewThread();
+
+        AddNativeAssets();
+
+        InitPlatform();
+
+        ExtensionLoader extensionLoader = new ExtensionLoader();
+        extensionLoader.LoadExtensions();
+        
+        return extensionLoader;
+    }
+
     private IPlatform GetActivePlatform()
     {
 #if STEAM

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

@@ -589,5 +589,8 @@
   "COPY_COLOR": "Copy color",
 
   "FAILED_DOWNLOADING_TITLE": "Downloading update failed",
-  "FAILED_DOWNLOADING": "Failed downloading the update, you might not have enough space on the disk"
+  "FAILED_DOWNLOADING": "Failed downloading the update, you might not have enough space on the disk",
+  
+  "CRASH_NOT_ALL_DOCUMENTS_RECOVERED_TITLE": "Could not recover all",
+  "CRASH_NOT_ALL_DOCUMENTS_RECOVERED": "Could not fully recover all files.\nIf you send the crash report to the developers\nthey might be able to help you."
 }

+ 13 - 3
src/PixiEditor/Helpers/CrashHelper.cs

@@ -112,14 +112,24 @@ internal class CrashHelper
         }
     }
 
-    public static async Task SendExceptionInfoToWebhook(Exception e, [CallerFilePath] string filePath = "<unknown>", [CallerMemberName] string memberName = "<unknown>")
+    public static void SendExceptionInfoToWebhook(Exception e, bool wait = false,
+        [CallerFilePath] string filePath = "<unknown>", [CallerMemberName] string memberName = "<unknown>")
+    {
+        var task = Task.Run(() => SendExceptionInfoToWebhookAsync(e, filePath, memberName));
+        if (wait)
+        {
+            task.Wait();
+        }
+    }
+
+    public static async Task SendExceptionInfoToWebhookAsync(Exception e, [CallerFilePath] string filePath = "<unknown>", [CallerMemberName] string memberName = "<unknown>")
     {
         if (DebugViewModel.IsDebugBuild)
             return;
-        await SendReportTextToWebhook(CrashReport.Generate(e), $"{filePath}; Method {memberName}");
+        await SendReportTextToWebhookAsync(CrashReport.Generate(e), $"{filePath}; Method {memberName}");
     }
 
-    public static async Task SendReportTextToWebhook(CrashReport report, string catchLocation = null)
+    public static async Task SendReportTextToWebhookAsync(CrashReport report, string catchLocation = null)
     {
         string reportText = report.ReportText;
         if (catchLocation is not null)

+ 2 - 2
src/PixiEditor/Models/AppExtensions/ExtensionLoader.cs

@@ -49,7 +49,7 @@ internal class ExtensionLoader
         }
         catch (Exception ex)
         {
-            Task.Run(async () => await CrashHelper.SendExceptionInfoToWebhook(ex));
+            CrashHelper.SendExceptionInfoToWebhook(ex);
         }
     }
 
@@ -86,7 +86,7 @@ internal class ExtensionLoader
         catch (Exception ex)
         {
             //MessageBox.Show(new LocalizedString("ERROR_LOADING_PACKAGE", packageJsonPath), "ERROR");
-            Task.Run(async () => await CrashHelper.SendExceptionInfoToWebhook(ex));
+            CrashHelper.SendExceptionInfoToWebhook(ex);
         }
     }
 

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

@@ -25,6 +25,8 @@ internal class CommandController
     public CommandCollection Commands { get; }
 
     public List<CommandGroup> CommandGroups { get; }
+    
+    public CommandLog.CommandLog Log { get; }
 
     public OneToManyDictionary<string, Command> FilterCommands { get; }
     
@@ -37,6 +39,7 @@ internal class CommandController
     public CommandController()
     {
         Current ??= this;
+        Log = new CommandLog.CommandLog();
 
         ShortcutsPath = Path.Join(
             Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),

+ 43 - 0
src/PixiEditor/Models/Commands/CommandLog/CommandLog.cs

@@ -0,0 +1,43 @@
+using System.Globalization;
+using System.Text;
+using PixiEditor.Models.Commands.Commands;
+
+namespace PixiEditor.Models.Commands.CommandLog;
+
+internal class CommandLog
+{
+    private readonly List<CommandLogEntry> list = new(MaxEntries);
+
+    private const int MaxEntries = 8;
+
+    public void Log(Command command, bool? canExecute)
+    {
+        if (canExecute.HasValue && !list[0].CanExecute.HasValue)
+        {
+            list[0].CanExecute = canExecute;
+            return;
+        }
+        
+        if (list.Count >= MaxEntries)
+        {
+            list.RemoveRange(MaxEntries - 1, list.Count - MaxEntries + 1);
+        }
+        
+        list.Insert(0, new CommandLogEntry(command, canExecute, DateTime.Now));
+    }
+
+    public string GetSummary(DateTime relativeTime)
+    {
+        var builder = new StringBuilder();
+
+        foreach (var entry in list)
+        {
+            var relativeSpan = entry.DateTime - relativeTime;
+            string canExecute = entry.CanExecute.HasValue ? entry.CanExecute.ToString() : "not executed";
+            
+            builder.AppendLine($"{entry.Command.InternalName} | CanExecute: {canExecute} | {relativeSpan.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture)}s ago | {entry.DateTime.ToString("O", CultureInfo.InvariantCulture)}");
+        }
+
+        return builder.ToString();
+    }
+}

+ 19 - 0
src/PixiEditor/Models/Commands/CommandLog/CommandLogEntry.cs

@@ -0,0 +1,19 @@
+using PixiEditor.Models.Commands.Commands;
+
+namespace PixiEditor.Models.Commands.CommandLog;
+
+internal class CommandLogEntry
+{
+    public Command Command { get; }
+
+    public bool? CanExecute { get; set; }
+    
+    public DateTime DateTime { get; }
+    
+    public CommandLogEntry(Command command, bool? commandMethod, DateTime dateTime)
+    {
+        Command = command;
+        CanExecute = commandMethod;
+        DateTime = dateTime;
+    }
+}

+ 17 - 2
src/PixiEditor/Models/Commands/CommandMethods.cs

@@ -18,11 +18,26 @@ internal class CommandMethods
 
     public void Execute(object parameter)
     {
-        if (CanExecute(parameter))
+        var log = CommandController.Current?.Log;
+        ToLog(log, null);
+        
+        if (!CanExecute(parameter))
         {
-            _execute(parameter);
+            ToLog(log, false);
+            return;
         }
+        ToLog(log, true);
+
+        _execute(parameter);
     }
 
     public bool CanExecute(object parameter) => _canExecute.CallEvaluate(_command, parameter);
+
+    private void ToLog(CommandLog.CommandLog? log, bool? canExecute)
+    {
+        if (log != null && _command != null)
+        {
+            log.Log(_command, canExecute);
+        }
+    }
 }

+ 263 - 39
src/PixiEditor/Models/DataHolders/CrashReport.cs

@@ -1,12 +1,19 @@
 using System.Diagnostics;
+using System.Globalization;
 using System.IO;
 using System.IO.Compression;
 using System.Reflection;
 using System.Text;
+using System.Windows.Input;
 using Newtonsoft.Json;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Common.UserPreferences;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Commands;
+using PixiEditor.Models.Enums;
 using PixiEditor.Parser;
 using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.Views;
 
 namespace PixiEditor.Models.DataHolders;
 
@@ -20,14 +27,59 @@ internal class CrashReport : IDisposable
 
         builder
             .AppendLine($"PixiEditor {VersionHelpers.GetCurrentAssemblyVersionString(moreSpecific: true)} x{IntPtr.Size * 8} crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss} {currentTime:zzz}")
+            .AppendLine($"Application started {GetFormatted(() => Process.GetCurrentProcess().StartTime, "yyyy.MM.dd HH:hh:ss")}, {GetFormatted(() => DateTime.Now - Process.GetCurrentProcess().StartTime, @"d\ hh\:mm\.ss")} ago")
             .AppendLine($"Report: {Guid.NewGuid()}\n")
             .AppendLine("-----System Information----")
             .AppendLine("General:")
             .AppendLine($"  OS: {Environment.OSVersion.VersionString}")
+            .AppendLine($"  Has Stylus Tablet Device: {GetFormatted(() => HasTabletDevice(TabletDeviceType.Stylus))}")
+            .AppendLine($"  Has Touch Tablet Device: {GetFormatted(() => HasTabletDevice(TabletDeviceType.Touch))}")
             .AppendLine();
 
         CrashHelper helper = new();
 
+        AppendHardwareInfo(helper, builder);
+
+        builder.AppendLine("\n--------Command Log--------\n");
+
+        try
+        {
+            builder.Append(CommandController.Current.Log.GetSummary(currentTime.LocalDateTime));
+        }
+        catch (Exception cemLogException)
+        {
+            builder.AppendLine($"Error ({cemLogException.GetType().FullName}: {cemLogException.Message}) while gathering command log, skipping...");
+        }
+        
+        builder.AppendLine("\n-----------State-----------");
+
+        try
+        {
+            AppendStateInfo(builder);
+        }
+        catch (Exception stateException)
+        {
+            builder.AppendLine($"Error ({stateException.GetType().FullName}: {stateException.Message}) while gathering state (Must be bug in GetPreferenceFormatted, GetFormatted or StringBuilder.AppendLine as these should not throw), skipping...");
+        }
+        
+        CrashHelper.AddExceptionMessage(builder, exception);
+
+        string filename = $"crash-{currentTime:yyyy-MM-dd_HH-mm-ss_fff}.zip";
+        string path = Path.Combine(
+            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+            "PixiEditor",
+            "crash_logs");
+        Directory.CreateDirectory(path);
+
+        CrashReport report = new();
+        report.FilePath = Path.Combine(path, filename);
+        report.ReportText = builder.ToString();
+
+        return report;
+    }
+
+    private static void AppendHardwareInfo(CrashHelper helper, StringBuilder builder)
+    {
         try
         {
             helper.GetCPUInformation(builder);
@@ -54,21 +106,146 @@ internal class CrashReport : IDisposable
         {
             builder.AppendLine($"Error ({memE.GetType().FullName}: {memE.Message}) while gathering memory information, skipping...");
         }
+    }
 
-        CrashHelper.AddExceptionMessage(builder, exception);
+    private static void AppendStateInfo(StringBuilder builder)
+    {
+        builder
+            .AppendLine("Environment:")
+            .AppendLine($"  Thread Count: {GetFormatted(() => Process.GetCurrentProcess().Threads.Count)}")
+            .AppendLine("\nCulture:")
+            .AppendLine($"  Selected language: {GetPreferenceFormatted("LanguageCode", true, "system")}")
+            .AppendLine($"  Current Culture: {GetFormatted(() => CultureInfo.CurrentCulture)}")
+            .AppendLine($"  Current UI Culture: {GetFormatted(() => CultureInfo.CurrentUICulture)}")
+            .AppendLine("\nPreferences:")
+            .AppendLine($"  Has shared toolbar enabled: {GetPreferenceFormatted("EnableSharedToolbar", true, false)}")
+            .AppendLine($"  Right click mode: {GetPreferenceFormatted<RightClickMode>("RightClickMode", true)}")
+            .AppendLine($"  Has Rich presence enabled: {GetPreferenceFormatted("EnableRichPresence", true, true)}")
+            .AppendLine($"  Debug Mode enabled: {GetPreferenceFormatted("IsDebugModeEnabled", true, false)}")
+            .AppendLine("\nUI:")
+            .AppendLine($"  MainWindow not null: {GetFormatted(() => MainWindow.Current != null)}")
+            .AppendLine($"  MainWindow Size: {GetFormatted(() => MainWindow.Current.RenderSize)}")
+            .AppendLine($"  MainWindow State: {GetFormatted(() => MainWindow.Current.WindowState)}")
+            .AppendLine("\nViewModels:")
+            .AppendLine($"  Has active updateable change: {GetFormatted(() => ViewModelMain.Current?.DocumentManagerSubViewModel?.ActiveDocument?.UpdateableChangeActive)}")
+            .AppendLine($"  Current Tool: {GetFormattedFromViewModelMain(x => x.ToolsSubViewModel?.ActiveTool?.ToolName)}")
+            .AppendLine($"  Primary Color: {GetFormattedFromViewModelMain(x => x.ColorsSubViewModel?.PrimaryColor)}")
+            .AppendLine($"  Secondary Color: {GetFormattedFromViewModelMain(x => x.ColorsSubViewModel?.SecondaryColor)}")
+            .Append("\nActive Document: ");
 
-        string filename = $"crash-{currentTime:yyyy-MM-dd_HH-mm-ss_fff}.zip";
-        string path = Path.Combine(
-            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
-            "PixiEditor",
-            "crash_logs");
-        Directory.CreateDirectory(path);
+        try
+        {
+            AppendActiveDocumentInfo(builder);
+        }
+        catch (Exception e)
+        {
+            builder.AppendLine($"Could not get active document info:\n{e}");
+        }
+    }
 
-        CrashReport report = new();
-        report.FilePath = Path.Combine(path, filename);
-        report.ReportText = builder.ToString();
+    private static void AppendActiveDocumentInfo(StringBuilder builder)
+    {
+        var main = ViewModelMain.Current;
+        
+        if (main == null)
+        {
+            builder.AppendLine("{ ViewModelMain.Current is null }");
+            return;
+        }
 
-        return report;
+        var manager = main.DocumentManagerSubViewModel;
+
+        if (manager == null)
+        {
+            builder.AppendLine("{ DocumentManagerSubViewModel is null }");
+            return;
+        }
+
+        var document = manager.ActiveDocument;
+
+        if (document == null)
+        {
+            builder.AppendLine("null");
+            return;
+        }
+
+        builder
+            .AppendLine()
+            .AppendLine($"  Size: {document.SizeBindable}")
+            .AppendLine($"  Layer Count: {FormatObject(document.StructureHelper.GetAllLayers().Count)}")
+            .AppendLine($"  Has all changes saved: {document.AllChangesSaved}")
+            .AppendLine($"  Horizontal Symmetry Enabled: {document.HorizontalSymmetryAxisEnabledBindable}")
+            .AppendLine($"  Horizontal Symmetry Value: {FormatObject(document.HorizontalSymmetryAxisYBindable)}")
+            .AppendLine($"  Vertical Symmetry Enabled: {document.VerticalSymmetryAxisEnabledBindable}")
+            .AppendLine($"  Vertical Symmetry Value: {FormatObject(document.VerticalSymmetryAxisXBindable)}")
+            .AppendLine($"  Updateable Change Active: {FormatObject(document.UpdateableChangeActive)}")
+            .AppendLine($"  Transform: {FormatObject(document.TransformViewModel)}");
+    }
+
+    private static bool HasTabletDevice(TabletDeviceType type) => Tablet.TabletDevices.Cast<TabletDevice>().Any(tabletDevice => tabletDevice.Type == type);
+
+    private static string GetPreferenceFormatted<T>(string name, bool roaming, T defaultValue = default, string? format = null)
+    {
+        try
+        {
+            var preferences = IPreferences.Current;
+
+            if (preferences == null)
+                return "{ Preferences are null }";
+            
+            var value = roaming
+                ? preferences.GetPreference(name, defaultValue)
+                : preferences.GetLocalPreference(name, defaultValue);
+
+            return FormatObject(value, format);
+        }
+        catch (Exception e)
+        {
+            return $$"""{ Failed getting preference: {{e.Message}} }""";
+        }
+    }
+
+    private static string GetFormattedFromViewModelMain<T>(Func<ViewModelMain, T?> getter, string? format = null)
+    {
+        var main = ViewModelMain.Current;
+        
+        if (main == null)
+            return "{ ViewModelMain.Current is null }";
+
+        return GetFormatted(() => getter(main), format);
+    }
+
+    private static string GetFormatted<T>(Func<T?> getter, string? format = null)
+    {
+        try
+        {
+            var value = getter();
+
+            return FormatObject(value, format);
+        }
+        catch (Exception e)
+        {
+            return $$"""{ Failed retrieving: {{e.Message}} }""";
+        }
+    }
+
+    private static string FormatObject<T>(T? value, string? format = null)
+    {
+        return value switch
+        {
+            null => "null",
+            IFormattable formattable => formattable.ToString(format, CultureInfo.InvariantCulture),
+            LocalizedString localizedS => FormatLocalizedString(localizedS),
+            string s => $"\"{s}\"",
+            _ => value.ToString()
+        };
+
+        string FormatLocalizedString(LocalizedString localizedS)
+        {
+            return localizedS.Parameters != null
+                ? $"{localizedS.Key} @({string.Join(", ", localizedS.Parameters.Select(x => FormatObject(x, format)))})" 
+                : localizedS.Key;
+        }
     }
 
     public static CrashReport Parse(string path)
@@ -90,38 +267,66 @@ internal class CrashReport : IDisposable
 
     public int GetDocumentCount() => ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")).Count();
 
-    public List<(string? originalPath, byte[] dotPixiBytes)> RecoverDocuments()
+    public bool TryRecoverDocuments(out List<RecoveredPixi> list)
     {
-        // Load .pixi files
-        Dictionary<string, byte[]> recoveredDocuments = new();
-        foreach (ZipArchiveEntry entry in ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")))
+        try
         {
-            using Stream stream = entry.Open();
-            using MemoryStream memStream = new();
-            stream.CopyTo(memStream);
-            recoveredDocuments.Add(entry.Name, memStream.ToArray());
+            list = RecoverDocuments();
+        }
+        catch (Exception e)
+        {
+            list = null;
+            CrashHelper.SendExceptionInfoToWebhook(e);
+            return false;
         }
 
-        ZipArchiveEntry? originalPathsEntry = ZipFile.Entries.Where(entry => entry.FullName == "DocumentInfo.json").FirstOrDefault();
-        if (originalPathsEntry is null)
-            return recoveredDocuments.Select<KeyValuePair<string, byte[]>, (string?, byte[])>(keyValue => (null, keyValue.Value)).ToList();
+        return true;
+    }
+    
+    public List<RecoveredPixi> RecoverDocuments()
+    {
+        List<RecoveredPixi> recoveredDocuments = new();
 
-        // Load original paths
-        Dictionary<string, string?> originalPaths;
+        var paths = TryGetOriginalPaths();
+        if (paths == null)
         {
-            using Stream stream = originalPathsEntry.Open();
-            using StreamReader reader = new(stream);
-            string json = reader.ReadToEnd();
-            originalPaths = JsonConvert.DeserializeObject<Dictionary<string, string?>>(json);
+            recoveredDocuments.AddRange(
+                ZipFile.Entries
+                    .Where(x => 
+                        x.FullName.StartsWith("Documents") && 
+                        x.FullName.EndsWith(".pixi"))
+                    .Select(entry => new RecoveredPixi(null, entry)));
+
+            return recoveredDocuments;
         }
 
-        return (
-            from docKeyValue in recoveredDocuments
-            join pathKeyValue in originalPaths on docKeyValue.Key equals pathKeyValue.Key
-            select (pathKeyValue.Value, docKeyValue.Value)
-            ).ToList();
+        recoveredDocuments.AddRange(paths.Select(path => new RecoveredPixi(path.Value, ZipFile.GetEntry($"Documents/{path.Key}"))));
+
+        return recoveredDocuments;
+
+        Dictionary<string, string>? TryGetOriginalPaths()
+        {
+            var originalPathsEntry = ZipFile.Entries.FirstOrDefault(entry => entry.FullName == "DocumentInfo.json");
+
+            if (originalPathsEntry == null)
+                return null;
+            
+            try
+            {
+                using var stream = originalPathsEntry.Open();
+                using var reader = new StreamReader(stream);
+                string json = reader.ReadToEnd();
+                
+                return JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
+            }
+            catch
+            {
+                return null;
+            }
+        }
     }
 
+
     public void Dispose()
     {
         ZipFile.Dispose();
@@ -169,13 +374,15 @@ internal class CrashReport : IDisposable
 
         // Write the documents into zip
         int counter = 0;
-        Dictionary<string, string?> originalPaths = new();
-        foreach (DocumentViewModel document in vm.DocumentManagerSubViewModel.Documents)
+        var originalPaths = new Dictionary<string, string>();
+        foreach (var document in vm.DocumentManagerSubViewModel.Documents)
         {
             try
             {
                 string fileName = string.IsNullOrWhiteSpace(document.FullFilePath) ? "Unsaved" : Path.GetFileNameWithoutExtension(document.FullFilePath);
-                string nameInZip = $"{fileName}-{document.OpenedUTC}-{counter}.pixi".Replace(':', '_');
+                string nameInZip = $"{fileName}-{document.OpenedUTC.ToString(CultureInfo.InvariantCulture)}-{counter.ToString(CultureInfo.InvariantCulture)}.pixi"
+                    .Replace(':', '_')
+                    .Replace('/', '_');
 
                 byte[] serialized = PixiParser.Serialize(document.ToSerializable());
 
@@ -209,10 +416,27 @@ internal class CrashReport : IDisposable
         ReportText = Encoding.UTF8.GetString(encodedReport);
     }
 
-    internal class CrashReportUserMessage
+
+    public class RecoveredPixi
     {
-        public string Message { get; set; }
+        public string? Path { get; }
+        
+        public ZipArchiveEntry RecoveredEntry { get; }
+        
+        public byte[] GetRecoveredBytes()
+        {
+            var buffer = new byte[RecoveredEntry.Length];
+            using var stream = RecoveredEntry.Open();
 
-        public string Mail { get; set; }
+            stream.ReadExactly(buffer);
+            
+            return buffer;
+        }
+        
+        public RecoveredPixi(string? path, ZipArchiveEntry recoveredEntry)
+        {
+            Path = path;
+            RecoveredEntry = recoveredEntry;
+        }
     }
 }

+ 5 - 1
src/PixiEditor/Models/Dialogs/OptionsDialog.cs

@@ -2,6 +2,7 @@
 using System.Windows.Controls;
 using System.Windows.Media;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.UI;
 using PixiEditor.Models.Localization;
 using PixiEditor.Views.Dialogs;
 
@@ -51,9 +52,12 @@ internal class OptionsDialog<T> : CustomDialog, IEnumerable<T>
         set => _results.Add(name, value);
     }
 
-    public override bool ShowDialog()
+    public override bool ShowDialog() => ShowDialog(false);
+
+    public bool ShowDialog(bool topmost)
     {
         var popup = new OptionPopup(Title, Content, new(_results.Keys.Select(x => (object)x)));
+        popup.Topmost = topmost;
         var popupResult = popup.ShowDialog();
 
         Result = (T)popup.Result;

+ 37 - 21
src/PixiEditor/Models/IO/Exporter.cs

@@ -92,33 +92,23 @@ internal class Exporter
 
         var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
 
-        if (typeFromPath != FileType.Pixi)
+        if (typeFromPath == FileType.Pixi)
         {
-            var maybeBitmap = document.MaybeRenderWholeImage();
-            if (maybeBitmap.IsT0)
-                return SaveResult.ConcurrencyError;
-            var bitmap = maybeBitmap.AsT1;
+            return TrySaveAsPixi(document, pathWithExtension);
+        }
 
-            if (!encodersFactory.ContainsKey(typeFromPath))
-            {
-                return SaveResult.UnknownError;
-            }
+        var maybeBitmap = document.MaybeRenderWholeImage();
+        if (maybeBitmap.IsT0)
+            return SaveResult.ConcurrencyError;
+        var bitmap = maybeBitmap.AsT1;
 
-            return TrySaveAs(encodersFactory[typeFromPath](), pathWithExtension, bitmap, exportSize);
-        }
-        else
+        if (!encodersFactory.ContainsKey(typeFromPath))
         {
-            try
-            {
-                Parser.PixiParser.Serialize(document.ToSerializable(), pathWithExtension);
-            }
-            catch (UnauthorizedAccessException)
-            {
-                return SaveResult.SecurityError;
-            }
+            return SaveResult.UnknownError;
         }
 
-        return SaveResult.Success;
+        return TrySaveAs(encodersFactory[typeFromPath](), pathWithExtension, bitmap, exportSize);
+
     }
 
     static Dictionary<FileType, Func<BitmapEncoder>> encodersFactory = new Dictionary<FileType, Func<BitmapEncoder>>();
@@ -179,6 +169,10 @@ internal class Exporter
         {
             return SaveResult.SecurityError;
         }
+        catch (UnauthorizedAccessException e)
+        {
+            return SaveResult.SecurityError;
+        }
         catch (IOException)
         {
             return SaveResult.IoError;
@@ -189,4 +183,26 @@ internal class Exporter
         }
         return SaveResult.Success;
     }
+
+    private static SaveResult TrySaveAsPixi(DocumentViewModel document, string pathWithExtension)
+    {
+        try
+        {
+            Parser.PixiParser.Serialize(document.ToSerializable(), pathWithExtension);
+        }
+        catch (UnauthorizedAccessException e)
+        {
+            return SaveResult.SecurityError;
+        }
+        catch (IOException)
+        {
+            return SaveResult.IoError;
+        }
+        catch
+        {
+            return SaveResult.UnknownError;
+        }
+
+        return SaveResult.Success;
+    }
 }

+ 2 - 2
src/PixiEditor/Models/Rendering/CanvasUpdater.cs

@@ -147,6 +147,8 @@ internal class CanvasUpdater
                 break;
             }
         }
+        
+        UpdateAffectedNonRerenderedChunks(chunksToRerender, chunkGatherer.MainImageArea);
 
         bool anythingToUpdate = false;
         foreach (var (_, chunks) in chunksToRerender)
@@ -156,8 +158,6 @@ internal class CanvasUpdater
         if (!anythingToUpdate)
             return new();
 
-        UpdateAffectedNonRerenderedChunks(chunksToRerender, chunkGatherer.MainImageArea);
-        
         List<IRenderInfo> infos = new();
         UpdateMainImage(chunksToRerender, updatingStoredChunks ? null : chunkGatherer.MainImageArea.GlobalArea.Value, infos);
         return infos;

+ 24 - 2
src/PixiEditor/ViewModels/CrashReportViewModel.cs

@@ -3,8 +3,11 @@ using System.IO;
 using System.Net.Http;
 using System.Text;
 using System.Windows;
+using System.Windows.Threading;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 
@@ -40,16 +43,35 @@ internal class CrashReportViewModel : ViewModelBase
         AttachDebuggerCommand = new(AttachDebugger);
 
         if (!IsDebugBuild)
-            _ = CrashHelper.SendReportTextToWebhook(report);
+            _ = CrashHelper.SendReportTextToWebhookAsync(report);
     }
 
     public void RecoverDocuments(object args)
     {
-        MainWindow window = MainWindow.CreateWithDocuments(CrashReport.RecoverDocuments());
+        MainWindow window = MainWindow.CreateWithRecoveredDocuments(CrashReport, out var showMissingFilesDialog);
 
         Application.Current.MainWindow = window;
         window.Show();
         hasRecoveredDocuments = false;
+
+        if (showMissingFilesDialog)
+        {
+            var dialog = new OptionsDialog<LocalizedString>(
+                "CRASH_NOT_ALL_DOCUMENTS_RECOVERED_TITLE",
+                new LocalizedString("CRASH_NOT_ALL_DOCUMENTS_RECOVERED"))
+            {
+                {
+                    "SEND", _ =>
+                    {
+                        var sendReportDialog = new SendCrashReportWindow(CrashReport);
+                        sendReportDialog.ShowDialog();
+                    }
+                },
+                "CLOSE"
+            };
+
+            dialog.ShowDialog(true);
+        }
     }
 
     [Conditional("DEBUG")]

+ 6 - 1
src/PixiEditor/ViewModels/SubViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs

@@ -1,4 +1,6 @@
-using System.Windows.Input;
+using System.Diagnostics;
+using System.Text;
+using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.Common.Localization;
@@ -10,6 +12,7 @@ using PixiEditor.Views.UserControls.Overlays.TransformOverlay;
 
 namespace PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
 #nullable enable
+[DebuggerDisplay("{ToString(),nq}")]
 internal class DocumentTransformViewModel : NotifyableObject
 {
     private DocumentViewModel document;
@@ -246,4 +249,6 @@ internal class DocumentTransformViewModel : NotifyableObject
                 break;
         }
     }
+
+    public override string ToString() => !TransformActive ? "Not active" : $"Transform Mode: {activeTransformMode}; Corner Freedom: {CornerFreedom}; Side Freedom: {SideFreedom}";
 }

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

@@ -102,8 +102,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         {
             OpenFromPath(file);
         }
-        else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0
-                  || !args.Contains("--crash")) && !args.Contains("--openedInExisting"))
+        else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0 && !args.Contains("--crash")) && !args.Contains("--openedInExisting"))
         {
             if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
             {
@@ -157,7 +156,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
                 continue;
             }
             
-            OpenFromPath(dataImage.name, false);
+            OpenRegularImage(dataImage.image, null);
         }
     }
 

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

@@ -20,7 +20,7 @@ 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 async Task OpenHyperlink(string url)
+    public static void OpenHyperlink(string url)
     {
         try
         {
@@ -28,8 +28,8 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
         }
         catch (Exception e)
         {
+            CrashHelper.SendExceptionInfoToWebhook(e);
             NoticeDialog.Show(title: "Error", message: $"Couldn't open the address {url} in your default browser");
-            await CrashHelper.SendExceptionInfoToWebhook(e);
         }
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs

@@ -222,7 +222,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             }
             catch (Exception e)
             {
-                CrashHelper.SendExceptionInfoToWebhook(e);
+                CrashHelper.SendExceptionInfoToWebhookAsync(e);
                 NoticeDialog.Show("COULD_NOT_CHECK_FOR_UPDATES", "UPDATE_CHECK_FAILED");
             }
 

+ 1 - 1
src/PixiEditor/Views/Dialogs/HelloTherePopup.xaml.cs

@@ -238,7 +238,7 @@ internal partial class HelloTherePopup : Window
         {
             IsFetchingNews = false;
             FailedFetchingNews = true;
-            await CrashHelper.SendExceptionInfoToWebhook(ex);
+            await CrashHelper.SendExceptionInfoToWebhookAsync(ex);
         }
     }
 }

+ 1 - 0
src/PixiEditor/Views/Dialogs/OptionsPopup.xaml.cs

@@ -39,6 +39,7 @@ internal partial class OptionPopup : Window
 
     public OptionPopup(string title, object content, ObservableCollection<object> options)
     {
+        Title = title;
         PopupContent = content;
         Options = options;
         CancelCommand = new RelayCommand(Cancel);

+ 48 - 5
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -16,11 +16,14 @@ using PixiEditor.Extensions.UI;
 using PixiEditor.Helpers;
 using PixiEditor.Models.AppExtensions;
 using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.IO;
 using PixiEditor.Platform;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.Views;
 
@@ -79,6 +82,14 @@ internal partial class MainWindow : Window
         });
 
         DataContext.DocumentManagerSubViewModel.ActiveDocumentChanged += DocumentChanged;
+        
+        ContentRendered += OnContentRendered;
+    }
+
+    private void OnContentRendered(object sender, EventArgs e)
+    {
+        LoadingWindow.Instance?.SafeClose();
+        Activate();
     }
 
     private void SetupTranslator()
@@ -96,17 +107,49 @@ internal partial class MainWindow : Window
         GlobalMouseHook.Instance.Initilize(this);
     }
 
-    public static MainWindow CreateWithDocuments(IEnumerable<(string? originalPath, byte[] dotPixiBytes)> documents)
+    public static MainWindow CreateWithRecoveredDocuments(CrashReport report, out bool showMissingFilesDialog)
     {
-        MainWindow window = new(extLoader);
-        FileViewModel fileVM = window.services.GetRequiredService<FileViewModel>();
+        var window = GetMainWindow();
+        var fileVM = window.services.GetRequiredService<FileViewModel>();
 
-        foreach (var (path, bytes) in documents)
+        if (!report.TryRecoverDocuments(out var documents))
         {
-            fileVM.OpenRecoveredDotPixi(path, bytes);
+            showMissingFilesDialog = true;
+            return window;
         }
 
+        var i = 0;
+
+        foreach (var document in documents)
+        {
+            try
+            {
+                fileVM.OpenRecoveredDotPixi(document.Path, document.GetRecoveredBytes());
+                i++;
+            }
+            catch (Exception e)
+            {
+                CrashHelper.SendExceptionInfoToWebhook(e);
+            }
+        }
+
+        showMissingFilesDialog = documents.Count != i;
+
         return window;
+
+        MainWindow GetMainWindow()
+        {
+            try
+            {
+                var app = (App)Application.Current;
+                return new MainWindow(app.InitApp());
+            }
+            catch (Exception e)
+            {
+                CrashHelper.SendExceptionInfoToWebhook(e, true);
+                throw;
+            }
+        }
     }
 
     /// <summary>Brings main window to foreground.</summary>