Browse Source

Refactor FileViewModel a bit;
Make document recovery work;
Fix document recovery crash (#405);
Fix .pixi filename clash in crash report (#387);
Don't use the global mutex when restarting pixieditor after crash

Equbuxu 2 years ago
parent
commit
6e3291db2b

+ 3 - 2
src/PixiEditor.DrawingApi.Skia/Implementations/SKObjectImplementation.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using SkiaSharp;
 using SkiaSharp;
 
 
@@ -6,12 +7,12 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
 {
 {
     public abstract class SkObjectImplementation<T> where T : SKObject
     public abstract class SkObjectImplementation<T> where T : SKObject
     {
     {
-        internal readonly Dictionary<IntPtr, T> ManagedInstances = new Dictionary<IntPtr, T>();
+        internal readonly ConcurrentDictionary<IntPtr, T> ManagedInstances = new ConcurrentDictionary<IntPtr, T>();
         
         
         public virtual void DisposeObject(IntPtr objPtr)
         public virtual void DisposeObject(IntPtr objPtr)
         {
         {
             ManagedInstances[objPtr].Dispose();
             ManagedInstances[objPtr].Dispose();
-            ManagedInstances.Remove(objPtr);
+            ManagedInstances.TryRemove(objPtr, out _);
         }
         }
         
         
         public T this[IntPtr objPtr]
         public T this[IntPtr objPtr]

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaBitmapImplementation.cs

@@ -12,7 +12,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             SKBitmap bitmap = ManagedInstances[objectPointer];
             SKBitmap bitmap = ManagedInstances[objectPointer];
             bitmap.Dispose();   
             bitmap.Dispose();   
             
             
-            ManagedInstances.Remove(objectPointer);
+            ManagedInstances.TryRemove(objectPointer, out _);
         }
         }
 
 
         public Bitmap Decode(ReadOnlySpan<byte> buffer)
         public Bitmap Decode(ReadOnlySpan<byte> buffer)

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs

@@ -173,7 +173,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         {
         {
             ManagedInstances[objectPointer].Dispose();
             ManagedInstances[objectPointer].Dispose();
             
             
-            ManagedInstances.Remove(objectPointer);
+            ManagedInstances.TryRemove(objectPointer, out _);
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaColorSpaceImplementation.cs

@@ -25,7 +25,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         {
         {
             if (objectPointer == _srgbPointer) return;
             if (objectPointer == _srgbPointer) return;
             ManagedInstances[objectPointer].Dispose();
             ManagedInstances[objectPointer].Dispose();
-            ManagedInstances.Remove(objectPointer);
+            ManagedInstances.TryRemove(objectPointer, out _);
         }
         }
     }
     }
 }
 }

+ 3 - 3
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImageImplementation.cs

@@ -10,7 +10,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
     public class SkiaImageImplementation : SkObjectImplementation<SKImage>, IImageImplementation
     public class SkiaImageImplementation : SkObjectImplementation<SKImage>, IImageImplementation
     {
     {
         private readonly SkObjectImplementation<SKData> _imgImplementation;
         private readonly SkObjectImplementation<SKData> _imgImplementation;
-        private SkObjectImplementation<SKSurface> _surfaceImplementation;
+        private SkObjectImplementation<SKSurface>? _surfaceImplementation;
         
         
         public SkiaImageImplementation(SkObjectImplementation<SKData> imgDataImplementation)
         public SkiaImageImplementation(SkObjectImplementation<SKData> imgDataImplementation)
         {
         {
@@ -24,7 +24,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         
         
         public Image Snapshot(DrawingSurface drawingSurface)
         public Image Snapshot(DrawingSurface drawingSurface)
         {
         {
-            var surface = _surfaceImplementation[drawingSurface.ObjectPointer];
+            var surface = _surfaceImplementation![drawingSurface.ObjectPointer];
             SKImage snapshot = surface.Snapshot();
             SKImage snapshot = surface.Snapshot();
             
             
             ManagedInstances[snapshot.Handle] = snapshot;
             ManagedInstances[snapshot.Handle] = snapshot;
@@ -42,7 +42,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         public void DisposeImage(Image image)
         public void DisposeImage(Image image)
         {
         {
             ManagedInstances[image.ObjectPointer].Dispose();
             ManagedInstances[image.ObjectPointer].Dispose();
-            ManagedInstances.Remove(image.ObjectPointer);
+            ManagedInstances.TryRemove(image.ObjectPointer, out _);
         }
         }
 
 
         public Image FromEncodedData(string path)
         public Image FromEncodedData(string path)

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImgDataImplementation.cs

@@ -14,7 +14,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             if (ManagedInstances.ContainsKey(objectPointer))
             if (ManagedInstances.ContainsKey(objectPointer))
             {
             {
                 ManagedInstances[objectPointer].Dispose();
                 ManagedInstances[objectPointer].Dispose();
-                ManagedInstances.Remove(objectPointer);
+                ManagedInstances.TryRemove(objectPointer, out _);
             }
             }
         }
         }
 
 

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaPaintImplementation.cs

@@ -21,7 +21,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             if (!ManagedInstances.ContainsKey(paintObjPointer)) return;
             if (!ManagedInstances.ContainsKey(paintObjPointer)) return;
             SKPaint paint = ManagedInstances[paintObjPointer];
             SKPaint paint = ManagedInstances[paintObjPointer];
             paint.Dispose();
             paint.Dispose();
-            ManagedInstances.Remove(paintObjPointer);
+            ManagedInstances.TryRemove(paintObjPointer, out _);
         }
         }
 
 
         public Paint Clone(IntPtr paintObjPointer)
         public Paint Clone(IntPtr paintObjPointer)

+ 5 - 5
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaPathImplementation.cs

@@ -33,7 +33,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         {
         {
             if (path.IsDisposed) return;
             if (path.IsDisposed) return;
             ManagedInstances[path.ObjectPointer].Dispose();
             ManagedInstances[path.ObjectPointer].Dispose();
-            ManagedInstances.Remove(path.ObjectPointer);
+            ManagedInstances.TryRemove(path.ObjectPointer, out _);
         }
         }
 
 
         public bool IsPathOval(VectorPath path)
         public bool IsPathOval(VectorPath path)
@@ -74,14 +74,14 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         public IntPtr Create()
         public IntPtr Create()
         {
         {
             SKPath path = new SKPath();
             SKPath path = new SKPath();
-            ManagedInstances.Add(path.Handle, path);
+            ManagedInstances[path.Handle] = path;
             return path.Handle;
             return path.Handle;
         }
         }
 
 
         public IntPtr Clone(VectorPath other)
         public IntPtr Clone(VectorPath other)
         {
         {
             SKPath path = new SKPath(ManagedInstances[other.ObjectPointer]);
             SKPath path = new SKPath(ManagedInstances[other.ObjectPointer]);
-            ManagedInstances.Add(path.Handle, path);
+            ManagedInstances[path.Handle] = path;
             return path.Handle;
             return path.Handle;
         }
         }
 
 
@@ -157,14 +157,14 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         public VectorPath Op(VectorPath vectorPath, VectorPath ellipsePath, VectorPathOp pathOp)
         public VectorPath Op(VectorPath vectorPath, VectorPath ellipsePath, VectorPathOp pathOp)
         {
         {
             SKPath skPath = ManagedInstances[vectorPath.ObjectPointer].Op(ManagedInstances[ellipsePath.ObjectPointer], (SKPathOp)pathOp);
             SKPath skPath = ManagedInstances[vectorPath.ObjectPointer].Op(ManagedInstances[ellipsePath.ObjectPointer], (SKPathOp)pathOp);
-            ManagedInstances.Add(skPath.Handle, skPath);
+            ManagedInstances[skPath.Handle] = skPath;
             return new VectorPath(skPath.Handle);
             return new VectorPath(skPath.Handle);
         }
         }
 
 
         public VectorPath Simplify(VectorPath path)
         public VectorPath Simplify(VectorPath path)
         {
         {
             SKPath skPath = ManagedInstances[path.ObjectPointer].Simplify();
             SKPath skPath = ManagedInstances[path.ObjectPointer].Simplify();
-            ManagedInstances.Add(skPath.Handle, skPath);
+            ManagedInstances[skPath.Handle] = skPath;
             return new VectorPath(skPath.Handle);
             return new VectorPath(skPath.Handle);
         }
         }
 
 

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaPixmapImplementation.cs

@@ -18,7 +18,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         public void Dispose(IntPtr objectPointer)
         public void Dispose(IntPtr objectPointer)
         {
         {
             ManagedInstances[objectPointer].Dispose();
             ManagedInstances[objectPointer].Dispose();
-            ManagedInstances.Remove(objectPointer);
+            ManagedInstances.TryRemove(objectPointer, out _);
         }
         }
 
 
         public IntPtr GetPixels(IntPtr objectPointer)
         public IntPtr GetPixels(IntPtr objectPointer)

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaSurfaceImplementation.cs

@@ -68,7 +68,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         public void Dispose(DrawingSurface drawingSurface)
         public void Dispose(DrawingSurface drawingSurface)
         {
         {
             ManagedInstances[drawingSurface.ObjectPointer].Dispose();
             ManagedInstances[drawingSurface.ObjectPointer].Dispose();
-            ManagedInstances.Remove(drawingSurface.ObjectPointer);
+            ManagedInstances.TryRemove(drawingSurface.ObjectPointer, out _);
         }
         }
 
 
         private DrawingSurface CreateDrawingSurface(SKSurface skSurface)
         private DrawingSurface CreateDrawingSurface(SKSurface skSurface)

+ 21 - 19
src/PixiEditor/App.xaml.cs

@@ -3,14 +3,13 @@ using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using PixiEditor.Views;
 using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 
 namespace PixiEditor;
 namespace PixiEditor;
 
 
-/// <summary>
-///     Interaction logic for App.xaml.
-/// </summary>
 internal partial class App : Application
 internal partial class App : Application
 {
 {
     /// <summary>The event mutex name.</summary>
     /// <summary>The event mutex name.</summary>
@@ -29,21 +28,20 @@ internal partial class App : Application
 
 
     protected override void OnStartup(StartupEventArgs e)
     protected override void OnStartup(StartupEventArgs e)
     {
     {
-        if (HandleNewInstance())
-        {
-            StartupArgs.Args = e.Args.ToList();
-            string arguments = string.Join(' ', e.Args);
+        StartupArgs.Args = e.Args.ToList();
+        string arguments = string.Join(' ', e.Args);
 
 
-            if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
-            {
-                CrashReport report = CrashReport.Parse(groups[2].Value);
-                MainWindow = new CrashReportDialog(report);
-            }
-            else
-            {
-                MainWindow = new MainWindow();
-            }
+        if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
+        {
+            CrashReport report = CrashReport.Parse(groups[2].Value);
+            MainWindow = new CrashReportDialog(report);
+            MainWindow.Show();
+            return;
+        }
 
 
+        if (HandleNewInstance())
+        {
+            MainWindow = new MainWindow();
             MainWindow.Show();
             MainWindow.Show();
         }
         }
     }
     }
@@ -99,12 +97,16 @@ internal partial class App : Application
     protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
     protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
     {
     {
         base.OnSessionEnding(e);
         base.OnSessionEnding(e);
-        /*
-        if (ViewModelMain.Current.BitmapManager.Documents.Any(x => !x.ChangesSaved))
+
+        var vm = ViewModelMain.Current;
+        if (vm is null)
+            return;
+
+        if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
         {
         {
             ConfirmationType confirmation = ConfirmationDialog.Show($"{e.ReasonSessionEnding} with unsaved data. Are you sure?", $"{e.ReasonSessionEnding}");
             ConfirmationType confirmation = ConfirmationDialog.Show($"{e.ReasonSessionEnding} with unsaved data. Are you sure?", $"{e.ReasonSessionEnding}");
             e.Cancel = confirmation != ConfirmationType.Yes;
             e.Cancel = confirmation != ConfirmationType.Yes;
-        }*/
+        }
     }
     }
 
 
     private bool ParseArgument(string pattern, string args, out Group[] groups)
     private bool ParseArgument(string pattern, string args, out Group[] groups)

+ 48 - 24
src/PixiEditor/Models/DataHolders/CrashReport.cs

@@ -3,11 +3,14 @@ using System.IO;
 using System.IO.Compression;
 using System.IO.Compression;
 using System.Reflection;
 using System.Reflection;
 using System.Text;
 using System.Text;
+using Newtonsoft.Json;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
+using PixiEditor.Parser;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
 
 namespace PixiEditor.Models.DataHolders;
 namespace PixiEditor.Models.DataHolders;
 
 
+#nullable enable
 internal class CrashReport : IDisposable
 internal class CrashReport : IDisposable
 {
 {
     public static CrashReport Generate(Exception exception)
     public static CrashReport Generate(Exception exception)
@@ -86,32 +89,36 @@ internal class CrashReport : IDisposable
 
 
     public int GetDocumentCount() => ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")).Count();
     public int GetDocumentCount() => ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")).Count();
 
 
-    public List<DocumentViewModel> RecoverDocuments()
+    public List<(string? originalPath, byte[] dotPixiBytes)> RecoverDocuments()
     {
     {
-        return new List<DocumentViewModel>();
-        /*
-        List<Document> documents = new();
+        // Load .pixi files
+        Dictionary<string, byte[]> recoveredDocuments = new();
         foreach (ZipArchiveEntry entry in ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")))
         foreach (ZipArchiveEntry entry in ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")))
         {
         {
             using Stream stream = entry.Open();
             using Stream stream = entry.Open();
+            using MemoryStream memStream = new();
+            stream.CopyTo(memStream);
+            recoveredDocuments.Add(entry.Name, memStream.ToArray());
+        }
 
 
-            Document document;
-
-            try
-            {
-                document = PixiParser.Deserialize(stream).ToDocument();
-                document.ChangesSaved = false;
-            }
-            catch
-            {
-                continue;
-            }
+        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();
 
 
-            documents.Add(document);
+        // Load original paths
+        Dictionary<string, string?> originalPaths;
+        {
+            using Stream stream = originalPathsEntry.Open();
+            using StreamReader reader = new(stream);
+            string json = reader.ReadToEnd();
+            originalPaths = JsonConvert.DeserializeObject<Dictionary<string, string?>>(json);
         }
         }
 
 
-        return documents;
-        */
+        return (
+            from docKeyValue in recoveredDocuments
+            join pathKeyValue in originalPaths on docKeyValue.Key equals pathKeyValue.Key
+            select (pathKeyValue.Value, docKeyValue.Value)
+            ).ToList();
     }
     }
 
 
     public void Dispose()
     public void Dispose()
@@ -147,7 +154,6 @@ internal class CrashReport : IDisposable
 
 
     public void Save()
     public void Save()
     {
     {
-        /*
         using FileStream zipStream = new(FilePath, FileMode.Create, FileAccess.Write);
         using FileStream zipStream = new(FilePath, FileMode.Create, FileAccess.Write);
         using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
         using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
 
 
@@ -156,21 +162,39 @@ internal class CrashReport : IDisposable
             reportStream.Write(Encoding.UTF8.GetBytes(ReportText));
             reportStream.Write(Encoding.UTF8.GetBytes(ReportText));
         }
         }
 
 
-        foreach (Document document in ViewModelMain.Current.BitmapManager.Documents)
+        var vm = ViewModelMain.Current;
+        if (vm is null)
+            return;
+
+        // Write the documents into zip
+        int counter = 0;
+        Dictionary<string, string?> originalPaths = new();
+        foreach (DocumentViewModel document in vm.DocumentManagerSubViewModel.Documents)
         {
         {
             try
             try
             {
             {
-                string documentPath =
-                    $"{(string.IsNullOrWhiteSpace(document.DocumentFilePath) ? "Unsaved" : Path.GetFileNameWithoutExtension(document.DocumentFilePath))}-{document.OpenedUTC}.pixi".Replace(':', '_');
+                string fileName = string.IsNullOrWhiteSpace(document.FullFilePath) ? "Unsaved" : Path.GetFileNameWithoutExtension(document.FullFilePath);
+                string nameInZip = $"{fileName}-{document.OpenedUTC}-{counter}.pixi".Replace(':', '_');
 
 
                 byte[] serialized = PixiParser.Serialize(document.ToSerializable());
                 byte[] serialized = PixiParser.Serialize(document.ToSerializable());
 
 
-                using Stream documentStream = archive.CreateEntry($"Documents/{documentPath}").Open();
+                using Stream documentStream = archive.CreateEntry($"Documents/{nameInZip}").Open();
                 documentStream.Write(serialized);
                 documentStream.Write(serialized);
+
+                originalPaths.Add(nameInZip, document.FullFilePath);
             }
             }
             catch { }
             catch { }
+            counter++;
+        }
+
+        // Write their original paths into a separate file
+        {
+            using Stream jsonStream = archive.CreateEntry("DocumentInfo.json").Open();
+            using StreamWriter writer = new StreamWriter(jsonStream);
+
+            string originalPathsJson = JsonConvert.SerializeObject(originalPaths, Formatting.Indented);
+            writer.Write(originalPathsJson);
         }
         }
-        */
     }
     }
 
 
     private void ExtractReport()
     private void ExtractReport()

+ 23 - 0
src/PixiEditor/Models/IO/Importer.cs

@@ -84,6 +84,29 @@ internal class Importer : NotifyableObject
         }
         }
     }
     }
 
 
+    public static DocumentViewModel ImportDocument(byte[] file, string? originalFilePath)
+    {
+        try
+        {
+            var doc = PixiParser.Deserialize(file).ToDocument();
+            doc.FullFilePath = originalFilePath;
+            return doc;
+        }
+        catch (InvalidFileException)
+        {
+            try
+            {
+                var doc = DepractedPixiParser.Deserialize(file).ToDocument();
+                doc.FullFilePath = originalFilePath;
+                return doc;
+            }
+            catch (InvalidFileException)
+            {
+                throw new CorruptedFileException();
+            }
+        }
+    }
+
     public static bool IsSupportedFile(string path)
     public static bool IsSupportedFile(string path)
     {
     {
         return SupportedFilesHelper.IsSupportedFile(path);
         return SupportedFilesHelper.IsSupportedFile(path);

+ 7 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -202,6 +202,7 @@ internal partial class DocumentViewModel : NotifyableObject
         AddMembers(viewModel.StructureRoot.GuidValue, builderInstance.Children);
         AddMembers(viewModel.StructureRoot.GuidValue, builderInstance.Children);
 
 
         acc.AddFinishedActions(new DeleteRecordedChanges_Action());
         acc.AddFinishedActions(new DeleteRecordedChanges_Action());
+        viewModel.MarkAsSaved();
 
 
         return viewModel;
         return viewModel;
 
 
@@ -271,6 +272,12 @@ internal partial class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(AllChangesSaved));
         RaisePropertyChanged(nameof(AllChangesSaved));
     }
     }
 
 
+    public void MarkAsUnsaved()
+    {
+        lastChangeOnSave = Guid.NewGuid();
+        RaisePropertyChanged(nameof(AllChangesSaved));
+    }
+
     /// <summary>
     /// <summary>
     /// Takes the selected area and converts it into a surface
     /// Takes the selected area and converts it into a surface
     /// </summary>
     /// </summary>

+ 110 - 114
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -1,5 +1,6 @@
 using System.IO;
 using System.IO;
 using System.Windows.Input;
 using System.Windows.Input;
+using System.Windows.Shapes;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Microsoft.Win32;
 using Microsoft.Win32;
@@ -59,84 +60,92 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         }
     }
     }
 
 
-    /// <summary>
-    ///     Generates new Layer and sets it as active one.
-    /// </summary>
-    /// <param name="parameter">CommandParameter.</param>
-    [Command.Basic("PixiEditor.File.New", "New image", "Create new image", Key = Key.N, Modifiers = ModifierKeys.Control)]
-    public void OpenNewFilePopup()
+    private void OpenHelloTherePopup()
     {
     {
-        NewFileDialog newFile = new NewFileDialog();
-        if (newFile.ShowDialog())
-        {
-            NewDocument(b => b
-                .WithSize(newFile.Width,newFile.Height)
-                .WithLayer(l => l
-                    .WithName("Base Layer")
-                    .WithSurface(new Surface(new VecI(newFile.Width, newFile.Height)))));
-        }
+        new HelloTherePopup(this).Show();
     }
     }
 
 
-    public void OpenHelloTherePopup()
+    private void Owner_OnStartupEvent(object sender, System.EventArgs e)
     {
     {
-        new HelloTherePopup(this).Show();
+        List<string> args = StartupArgs.Args;
+        string file = args.FirstOrDefault(x => Importer.IsSupportedFile(x) && File.Exists(x));
+        if (file != null)
+        {
+            OpenFromPath(file);
+        }
+        else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0
+                  || !args.Contains("--crash")) && !args.Contains("--openedInExisting"))
+        {
+            if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
+            {
+                OpenHelloTherePopup();
+            }
+        }
     }
     }
 
 
-    public DocumentViewModel NewDocument(Action<DocumentViewModelBuilder> builder)
+    [Command.Internal("PixiEditor.File.OpenRecent")]
+    public void OpenRecent(object parameter)
     {
     {
-        var doc = DocumentViewModel.Build(builder);
-
-        doc.MarkAsSaved();
-        Owner.DocumentManagerSubViewModel.Documents.Add(doc);
-        Owner.WindowSubViewModel.CreateNewViewport(doc);
-        Owner.WindowSubViewModel.MakeDocumentViewportActive(doc);
+        string path = (string)parameter;
+        if (!File.Exists(path))
+        {
+            NoticeDialog.Show("The file does not exist", "Failed to open the file");
+            RecentlyOpened.Remove(path);
+            IPreferences.Current.UpdateLocalPreference("RecentlyOpened", RecentlyOpened.Select(x => x.FilePath));
+            return;
+        }
 
 
-        return doc;
+        OpenFromPath(path);
     }
     }
 
 
-    /// <summary>
-    ///     Opens file from path.
-    /// </summary>
-    /// <param name="path">Path to file.</param>
-    public void OpenFile(string path)
+    [Command.Basic("PixiEditor.File.Open", "Open", "Open file", Key = Key.O, Modifiers = ModifierKeys.Control)]
+    public void OpenFromOpenFileDialog()
     {
     {
-        ImportFileDialog dialog = new ImportFileDialog();
+        string filter = SupportedFilesHelper.BuildOpenFilter();
 
 
-        if (path != null && File.Exists(path))
+        OpenFileDialog dialog = new OpenFileDialog
         {
         {
-            dialog.FilePath = path;
-        }
+            Filter = filter,
+            FilterIndex = 0
+        };
 
 
-        if (dialog.ShowDialog())
+        if (!(bool)dialog.ShowDialog() || !Importer.IsSupportedFile(dialog.FileName))
+            return;
+
+        OpenFromPath(dialog.FileName);
+    }
+
+    private bool MakeExistingDocumentActiveIfOpened(string path)
+    {
+        foreach (DocumentViewModel document in Owner.DocumentManagerSubViewModel.Documents)
         {
         {
-            DocumentViewModel doc = NewDocument(b => b
-                .WithSize(dialog.FileWidth, dialog.FileHeight)
-                .WithLayer(l => l
-                    .WithName("Image")
-                    .WithSize(dialog.FileWidth, dialog.FileHeight)
-                    .WithSurface(Importer.ImportImage(dialog.FilePath, new VecI(dialog.FileWidth, dialog.FileHeight)))));
-            doc.FullFilePath = path;
+            if (document.FullFilePath is not null && System.IO.Path.GetFullPath(document.FullFilePath) == System.IO.Path.GetFullPath(path))
+            {
+                Owner.WindowSubViewModel.MakeDocumentViewportActive(document);
+                return true;
+            }
         }
         }
+        return false;
     }
     }
 
 
-    [Command.Basic("PixiEditor.File.Open", "Open", "Open file", Key = Key.O, Modifiers = ModifierKeys.Control)]
-    public void Open(string path)
+    /// <summary>
+    /// Tries to open the passed file if it isn't already open
+    /// </summary>
+    /// <param name="path"></param>
+    public void OpenFromPath(string path)
     {
     {
-        if (path == null)
-        {
-            Open();
+        if (MakeExistingDocumentActiveIfOpened(path))
             return;
             return;
-        }
 
 
         try
         try
         {
         {
             if (path.EndsWith(".pixi"))
             if (path.EndsWith(".pixi"))
             {
             {
-                OpenDocument(path);
+                OpenDotPixi(path);
             }
             }
             else
             else
             {
             {
-                OpenFile(path);
+                OpenRegularImage(path);
             }
             }
         }
         }
         catch (CorruptedFileException ex)
         catch (CorruptedFileException ex)
@@ -149,88 +158,75 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         }
     }
     }
 
 
-    private void Owner_OnStartupEvent(object sender, System.EventArgs e)
+    /// <summary>
+    /// Opens a .pixi file from path, creates a document from it, and adds it to the system
+    /// </summary>
+    private void OpenDotPixi(string path)
     {
     {
-        List<string> args = StartupArgs.Args;
-        string file = args.FirstOrDefault(x => Importer.IsSupportedFile(x) && File.Exists(x));
-        if (file != null)
-        {
-            Open(file);
-        }
-        else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0
-                  || !args.Contains("--crash")) && !args.Contains("--openedInExisting"))
-        {
-            if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
-            {
-                OpenHelloTherePopup();
-            }
-        }
+        DocumentViewModel document = Importer.ImportDocument(path);
+        AddDocumentViewModelToTheSystem(document);
     }
     }
 
 
-    [Command.Internal("PixiEditor.File.OpenRecent")]
+    /// <summary>
+    /// Opens a .pixi file from path, creates a document from it, and adds it to the system
+    /// </summary>
+    public void OpenRecoveredDotPixi(string? originalPath, byte[] dotPixiBytes)
+    {
+        DocumentViewModel document = Importer.ImportDocument(dotPixiBytes, originalPath);
+        document.MarkAsUnsaved();
+        AddDocumentViewModelToTheSystem(document);
+    }
 
 
-    public void OpenRecent(object parameter)
+    /// <summary>
+    /// Opens a regular image file from path, creates a document from it, and adds it to the system.
+    /// </summary>
+    private void OpenRegularImage(string path)
     {
     {
-        string path = (string)parameter;
+        ImportFileDialog dialog = new ImportFileDialog();
 
 
-        foreach (DocumentViewModel document in Owner.DocumentManagerSubViewModel.Documents)
+        if (path != null && File.Exists(path))
         {
         {
-            if (document.FullFilePath is not null && document.FullFilePath == path)
-            {
-                Owner.WindowSubViewModel.MakeDocumentViewportActive(document);
-                return;
-            }
+            dialog.FilePath = path;
         }
         }
 
 
-        if (!File.Exists(path))
+        if (dialog.ShowDialog())
         {
         {
-            NoticeDialog.Show("The file does not exist", "Failed to open the file");
-            RecentlyOpened.Remove(path);
-            IPreferences.Current.UpdateLocalPreference("RecentlyOpened", RecentlyOpened.Select(x => x.FilePath));
-            return;
+            DocumentViewModel doc = NewDocument(b => b
+                .WithSize(dialog.FileWidth, dialog.FileHeight)
+                .WithLayer(l => l
+                    .WithName("Image")
+                    .WithSize(dialog.FileWidth, dialog.FileHeight)
+                    .WithSurface(Importer.ImportImage(dialog.FilePath, new VecI(dialog.FileWidth, dialog.FileHeight)))));
+            doc.FullFilePath = path;
         }
         }
-
-        Open((string)parameter);
     }
     }
 
 
-    public void Open()
+    [Command.Basic("PixiEditor.File.New", "New image", "Create new image", Key = Key.N, Modifiers = ModifierKeys.Control)]
+    public void CreateFromNewFileDialog()
     {
     {
-        string filter = SupportedFilesHelper.BuildOpenFilter();
-
-        OpenFileDialog dialog = new OpenFileDialog
-        {
-            Filter = filter,
-            FilterIndex = 0
-        };
-
-        if ((bool)dialog.ShowDialog())
+        NewFileDialog newFile = new NewFileDialog();
+        if (newFile.ShowDialog())
         {
         {
-            if (Importer.IsSupportedFile(dialog.FileName))
-            {
-                Open(dialog.FileName);
-
-                if (Owner.DocumentManagerSubViewModel.Documents.Count > 0)
-                {
-                    Owner.WindowSubViewModel.MakeDocumentViewportActive(Owner.DocumentManagerSubViewModel.Documents[^1]);
-                }
-            }
+            NewDocument(b => b
+                .WithSize(newFile.Width, newFile.Height)
+                .WithLayer(l => l
+                    .WithName("Base Layer")
+                    .WithSurface(new Surface(new VecI(newFile.Width, newFile.Height)))));
         }
         }
     }
     }
 
 
-    private void OpenDocument(string path)
+    private DocumentViewModel NewDocument(Action<DocumentViewModelBuilder> builder)
     {
     {
-        DocumentViewModel document = Importer.ImportDocument(path);
-        DocumentManagerViewModel manager = Owner.DocumentManagerSubViewModel;
-        if (manager.Documents.Select(x => x.FullFilePath).All(y => y != path))
-        {
-            manager.Documents.Add(document);
-            Owner.WindowSubViewModel.CreateNewViewport(document);
-            Owner.WindowSubViewModel.MakeDocumentViewportActive(document);
-        }
-        else
-        {
-            Owner.WindowSubViewModel.MakeDocumentViewportActive(manager.Documents.First(y => y.FullFilePath == path));
-        }
+        var doc = DocumentViewModel.Build(builder);
+        AddDocumentViewModelToTheSystem(doc);
+        return doc;
+    }
+
+    private void AddDocumentViewModelToTheSystem(DocumentViewModel doc)
+    {
+        Owner.DocumentManagerSubViewModel.Documents.Add(doc);
+        Owner.WindowSubViewModel.CreateNewViewport(doc);
+        Owner.WindowSubViewModel.MakeDocumentViewportActive(doc);
     }
     }
 
 
     [Command.Basic("PixiEditor.File.Save", false, "Save", "Save image", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control, IconPath = "Save.png")]
     [Command.Basic("PixiEditor.File.Save", false, "Save", "Save image", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control, IconPath = "Save.png")]
@@ -257,9 +253,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             success = path != null;
             success = path != null;
         }
         }
 
 
-        document.FullFilePath = path;
         if (success)
         if (success)
         {
         {
+            document.FullFilePath = path;
             document.MarkAsSaved();
             document.MarkAsSaved();
         }
         }
 
 

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

@@ -93,14 +93,14 @@ internal partial class HelloTherePopup : Window
     {
     {
         Application.Current.MainWindow.Activate();
         Application.Current.MainWindow.Activate();
         Close();
         Close();
-        FileViewModel.Open();
+        FileViewModel.OpenFromOpenFileDialog();
     }
     }
 
 
     private void OpenNewFile(object parameter)
     private void OpenNewFile(object parameter)
     {
     {
         Application.Current.MainWindow.Activate();
         Application.Current.MainWindow.Activate();
         Close();
         Close();
-        FileViewModel.OpenNewFilePopup();
+        FileViewModel.CreateFromNewFileDialog();
     }
     }
 
 
     private void OpenRecent(object parameter)
     private void OpenRecent(object parameter)

+ 6 - 10
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -58,21 +58,17 @@ internal partial class MainWindow : Window
         });
         });
     }
     }
 
 
-    public static MainWindow CreateWithDocuments(IEnumerable<DocumentViewModel> documents)
+    public static MainWindow CreateWithDocuments(IEnumerable<(string? originalPath, byte[] dotPixiBytes)> documents)
     {
     {
-        /*
         MainWindow window = new();
         MainWindow window = new();
-        BitmapManager bitmapManager = window.services.GetRequiredService<BitmapManager>();
+        FileViewModel fileVM = window.services.GetRequiredService<FileViewModel>();
 
 
-        foreach (Document document in documents)
+        foreach (var (path, bytes) in documents)
         {
         {
-            bitmapManager.Documents.Add(document);
+            fileVM.OpenRecoveredDotPixi(path, bytes);
         }
         }
 
 
-        bitmapManager.ActiveDocument = bitmapManager.Documents.FirstOrDefault();
-
-        return window;*/
-        return null;
+        return window;
     }
     }
 
 
     /// <summary>Brings main window to foreground.</summary>
     /// <summary>Brings main window to foreground.</summary>
@@ -194,7 +190,7 @@ internal partial class MainWindow : Window
             {
             {
                 if (Importer.IsSupportedFile(files[0]))
                 if (Importer.IsSupportedFile(files[0]))
                 {
                 {
-                    DataContext.FileSubViewModel.Open(files[0]);
+                    DataContext.FileSubViewModel.OpenFromPath(files[0]);
                 }
                 }
             }
             }
         }
         }