Browse Source

Multiple windows for the same image, random improvements

Equbuxu 3 years ago
parent
commit
25f83f25ea

+ 1 - 0
src/ChunkyImageLib/Chunk.cs

@@ -62,6 +62,7 @@ public class Chunk : IDisposable
     {
         if (returned)
             return;
+        Surface.SkiaSurface.Canvas.Flush();
         returned = true;
         Interlocked.Decrement(ref chunkCounter);
         ChunkPool.Instance.Push(this);

+ 1 - 1
src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -33,7 +33,7 @@ internal static class ServiceCollectionHelpers
         .AddSingleton<ClipboardViewModel>()
         .AddSingleton<UndoViewModel>()
         .AddSingleton<SelectionViewModel>()
-        .AddSingleton<ViewportViewModel>()
+        .AddSingleton<ViewOptionsViewModel>()
         .AddSingleton<ColorsViewModel>()
         .AddSingleton<RegistryViewModel>()
         .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModelMain>(), "764168193685979138"))

+ 1 - 2
src/PixiEditor/Helpers/UI/DocumentsTemplateSelector.cs

@@ -1,6 +1,5 @@
 using System.Windows;
 using System.Windows.Controls;
-using PixiEditor.ViewModels.SubViewModels.Document;
 
 namespace PixiEditor.Helpers.UI;
 
@@ -15,7 +14,7 @@ internal class DocumentsTemplateSelector : DataTemplateSelector
 
     public override DataTemplate SelectTemplate(object item, DependencyObject container)
     {
-        if (item is DocumentViewModel)
+        if (item is ViewportWindowViewModel)
         {
             return DocumentsViewTemplate;
         }

+ 1 - 3
src/PixiEditor/Helpers/UI/PanelsStyleSelector.cs

@@ -1,7 +1,5 @@
 using System.Windows;
 using System.Windows.Controls;
-using PixiEditor.Models.DataHolders;
-using PixiEditor.ViewModels.SubViewModels.Document;
 
 namespace PixiEditor.Helpers.UI;
 
@@ -11,7 +9,7 @@ internal class PanelsStyleSelector : StyleSelector
 
     public override Style SelectStyle(object item, DependencyObject container)
     {
-        if (item is DocumentViewModel)
+        if (item is ViewportWindowViewModel)
         {
             return DocumentTabStyle;
         }

+ 11 - 13
src/PixiEditor/Models/DataHolders/DocumentSizeChangedEventArgs.cs

@@ -1,20 +1,18 @@
-namespace PixiEditor.Models.DataHolders;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.Models.DataHolders;
 
 internal class DocumentSizeChangedEventArgs
 {
-    public DocumentSizeChangedEventArgs(int oldWidth, int oldHeight, int newWidth, int newHeight)
+    public DocumentSizeChangedEventArgs(DocumentViewModel document, VecI oldSize, VecI newSize)
     {
-        OldWidth = oldWidth;
-        OldHeight = oldHeight;
-        NewWidth = newWidth;
-        NewHeight = newHeight;
+        Document = document;
+        OldSize = oldSize;
+        NewSize = newSize;
     }
 
-    public int OldWidth { get; set; }
-
-    public int OldHeight { get; set; }
-
-    public int NewWidth { get; set; }
-
-    public int NewHeight { get; set; }
+    public VecI OldSize { get; }
+    public VecI NewSize { get; }
+    public DocumentViewModel Document { get; }
 }

+ 3 - 1
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -270,6 +270,8 @@ internal class DocumentUpdater
 
     private void ProcessSize(Size_ChangeInfo info)
     {
+        VecI oldSize = doc.SizeBindable;
+
         Dictionary<ChunkResolution, WriteableBitmap> newBitmaps = new();
         foreach ((ChunkResolution res, SKSurface surf) in doc.Surfaces)
         {
@@ -294,7 +296,7 @@ internal class DocumentUpdater
 
         UpdateMemberBitmapsRecursively(doc.StructureRoot, previewSize);
 
-        doc.CenterViewportTrigger.Execute(doc, doc.SizeBindable);
+        doc.InternalRaiseSizeChanged(new(doc, oldSize, info.Size));
     }
 
     private WriteableBitmap CreateBitmap(VecI size)

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

@@ -27,6 +27,7 @@ internal class FloodFillToolExecutor : UpdateableChangeExecutor
         if (!drawOnMask && member is not LayerViewModel)
             return ExecutionState.Error;
 
+        colorsVM.AddSwatch(color);
         memberGuid = member.GuidValue;
         considerAllLayers = fillTool.ConsiderAllLayers;
         color = colorsVM.PrimaryColor;

+ 4 - 0
src/PixiEditor/Models/Position/ViewportInfo.cs

@@ -1,6 +1,10 @@
 using ChunkyImageLib.DataHolders;
 
 namespace PixiEditor.Models.Position;
+
+/// <summary>
+/// Used to keep track of viewports inside DocumentViewModel without directly referencing them
+/// </summary>
 internal readonly record struct ViewportInfo(
     double Angle,
     VecD Center,

+ 1 - 1
src/PixiEditor/Styles/AvalonDock/Themes/Generic.xaml

@@ -1381,7 +1381,7 @@
                                 <StackPanel Orientation="Horizontal">
                                     <Image Stretch="Uniform" Name="previewImage" Margin="1" Width="30" Height="20"
                                            RenderOptions.BitmapScalingMode="NearestNeighbor"
-                                           Source="{Binding LayoutItem.Model.PreviewBitmap, RelativeSource={RelativeSource TemplatedParent}}"/>
+                                           Source="{Binding LayoutItem.Model.Document.PreviewBitmap, RelativeSource={RelativeSource TemplatedParent}}"/>
                                     <ContentPresenter
 									Margin="4,0"
 									Content="{Binding Model, RelativeSource={RelativeSource TemplatedParent}}"

+ 6 - 60
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs

@@ -4,7 +4,6 @@ using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Events;
-using PixiEditor.ViewModels.SubViewModels.Tools;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
 
 namespace PixiEditor.ViewModels.SubViewModels.Document;
@@ -12,8 +11,6 @@ namespace PixiEditor.ViewModels.SubViewModels.Document;
 [Command.Group("PixiEditor.Document", "Image")]
 internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
 {
-    public DocumentManagerViewModel(ViewModelMain owner) : base(owner) { }
-
     public ObservableCollection<DocumentViewModel> Documents { get; set; } = new ObservableCollection<DocumentViewModel>();
     public event EventHandler<DocumentChangedEventArgs>? ActiveDocumentChanged;
 
@@ -22,7 +19,8 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
     public DocumentViewModel? ActiveDocument
     {
         get => activeDocument;
-        set
+        // Use WindowSubViewModel.MakeDocumentViewportActive(document);
+        private set
         {
             if (activeDocument == value)
                 return;
@@ -30,77 +28,25 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
             activeDocument = value;
             RaisePropertyChanged(nameof(ActiveDocument));
             ActiveDocumentChanged?.Invoke(this, new(value, prevDoc));
-            ActiveWindow = value;
         }
     }
 
-    private object? activeWindow;
-    public object? ActiveWindow
+    public DocumentManagerViewModel(ViewModelMain owner) : base(owner)
     {
-        get => activeWindow;
-        set
-        {
-            if (activeWindow == value)
-                return;
-            activeWindow = value;
-            RaisePropertyChanged(nameof(ActiveWindow));
-            if (activeWindow is DocumentViewModel doc)
-                ActiveDocument = doc;
-        }
+        owner.WindowSubViewModel.ActiveViewportChanged += (_, args) => ActiveDocument = args.Document;
     }
 
+    public void MakeActiveDocumentNull() => ActiveDocument = null;
+
     [Evaluator.CanExecute("PixiEditor.HasDocument")]
     public bool DocumentNotNull() => ActiveDocument != null;
 
-    public void CloseDocument(Guid documentGuid)
-    {
-        /*
-        int nextIndex = 0;
-        if (document == ActiveDocument)
-        {
-            nextIndex = Documents.Count > 1 ? Documents.IndexOf(document) : -1;
-            nextIndex += nextIndex > 0 ? -1 : 0;
-        }
-
-        Documents.Remove(document);
-        ActiveDocument = nextIndex >= 0 ? Documents[nextIndex] : null;
-        */
-    }
-
-    public void UpdateActionDisplay(ToolViewModel tool)
-    {
-        //tool?.UpdateActionDisplay(ToolSessionController.IsCtrlDown, ToolSessionController.IsShiftDown, ToolSessionController.IsAltDown);
-    }
-
     [Command.Basic("PixiEditor.Document.ClipCanvas", "Clip Canvas", "Clip Canvas", CanExecute = "PixiEditor.HasDocument")]
     public void ClipCanvas()
     {
         //Owner.BitmapManager.ActiveDocument?.ClipCanvas();
     }
 
-    /*
-    public void RequestCloseDocument(Document document)
-    {
-        /*
-        if (!document.ChangesSaved)
-        {
-            ConfirmationType result = ConfirmationDialog.Show(ConfirmationDialogMessage, ConfirmationDialogTitle);
-            if (result == ConfirmationType.Yes)
-            {
-                Owner.FileSubViewModel.SaveDocument(false);
-                if (!document.ChangesSaved)
-                    return;
-            }
-            else if (result == ConfirmationType.Canceled)
-            {
-                return;
-            }
-        }
-
-        Owner.BitmapManager.CloseDocument(document);
-        
-    }
-*/
     [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "Toggle vertical symmetry axis", "Toggle vertical symmetry axis", CanExecute = "PixiEditor.HasDocument")]
     public void ToggleVerticalSymmetryAxis()
     {

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

@@ -21,6 +21,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Document;
 internal class DocumentViewModel : NotifyableObject
 {
     public event EventHandler<LayersChangedEventArgs>? LayersChanged;
+    public event EventHandler<DocumentSizeChangedEventArgs>? SizeChanged;
 
     private bool busy = false;
     public bool Busy
@@ -143,8 +144,6 @@ internal class DocumentViewModel : NotifyableObject
     public WpfObservableRangeCollection<SKColor> Swatches { get; set; } = new WpfObservableRangeCollection<SKColor>();
     public WpfObservableRangeCollection<SKColor> Palette { get; set; } = new WpfObservableRangeCollection<SKColor>();
 
-    public ExecutionTrigger<VecI> CenterViewportTrigger { get; } = new ExecutionTrigger<VecI>();
-    public ExecutionTrigger<double> ZoomViewportTrigger { get; } = new ExecutionTrigger<double>();
     public DocumentTransformViewModel TransformViewModel { get; }
 
 
@@ -348,6 +347,8 @@ internal class DocumentViewModel : NotifyableObject
 
     public void InternalRaiseLayersChanged(LayersChangedEventArgs args) => LayersChanged?.Invoke(this, args);
 
+    public void InternalRaiseSizeChanged(DocumentSizeChangedEventArgs args) => SizeChanged?.Invoke(this, args);
+
     public void InternalSetVerticalSymmetryAxisEnabled(bool verticalSymmetryAxisEnabled)
     {
         this.verticalSymmetryAxisEnabled = verticalSymmetryAxisEnabled;

+ 15 - 8
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -80,7 +80,6 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     {
         DocumentViewModel doc = new DocumentViewModel();
         Owner.DocumentManagerSubViewModel.Documents.Add(doc);
-        Owner.DocumentManagerSubViewModel.ActiveDocument = Owner.DocumentManagerSubViewModel.Documents[^1];
 
         if (doc.SizeBindable != size)
             doc.Operations.ResizeCanvas(size, ResizeAnchor.TopLeft);
@@ -88,6 +87,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             doc.Operations.CreateStructureMember(StructureMemberType.Layer);
         doc.Operations.ClearUndo();
         doc.MarkAsSaved();
+        Owner.WindowSubViewModel.CreateNewViewport(doc);
+        Owner.WindowSubViewModel.MakeDocumentViewportActive(doc);
 
         return doc;
     }
@@ -173,7 +174,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         {
             if (document.FullFilePath is not null && document.FullFilePath == path)
             {
-                Owner.DocumentManagerSubViewModel.ActiveDocument = document;
+                Owner.WindowSubViewModel.MakeDocumentViewportActive(document);
                 return;
             }
         }
@@ -207,7 +208,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
                 if (Owner.DocumentManagerSubViewModel.Documents.Count > 0)
                 {
-                    Owner.DocumentManagerSubViewModel.ActiveDocument = Owner.DocumentManagerSubViewModel.Documents[^1];
+                    Owner.WindowSubViewModel.MakeDocumentViewportActive(Owner.DocumentManagerSubViewModel.Documents[^1]);
                 }
             }
         }
@@ -220,22 +221,27 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         if (manager.Documents.Select(x => x.FullFilePath).All(y => y != path))
         {
             manager.Documents.Add(document);
-            manager.ActiveDocument = manager.Documents[^1];
+            Owner.WindowSubViewModel.MakeDocumentViewportActive(document);
         }
         else
         {
-            manager.ActiveDocument = manager.Documents.First(y => y.FullFilePath == path);
+            Owner.WindowSubViewModel.MakeDocumentViewportActive(manager.Documents.First(y => y.FullFilePath == path));
         }
     }
 
     [Command.Basic("PixiEditor.File.Save", false, "Save", "Save image", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control)]
     [Command.Basic("PixiEditor.File.SaveAsNew", true, "Save as...", "Save image as new", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control | ModifierKeys.Shift)]
-    public void SaveDocument(bool asNew)
+    public bool SaveActiveDocument(bool asNew)
     {
         DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
-            return;
-        if (asNew || string.IsNullOrEmpty(doc.FullFilePath))
+            return false;
+        return SaveDocument(doc, asNew);
+    }
+
+    public bool SaveDocument(DocumentViewModel document, bool asNew)
+    {
+        if (asNew || string.IsNullOrEmpty(document.FullFilePath))
         {
             //doc.SaveWithDialog();
         }
@@ -243,6 +249,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         {
             //doc.Save();
         }
+        return false;
     }
 
     /// <summary>

+ 7 - 5
src/PixiEditor/ViewModels/SubViewModels/Main/ViewportViewModel.cs → src/PixiEditor/ViewModels/SubViewModels/Main/ViewOptionsViewModel.cs

@@ -2,8 +2,8 @@
 using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
-
-internal class ViewportViewModel : SubViewModel<ViewModelMain>
+#nullable enable
+internal class ViewOptionsViewModel : SubViewModel<ViewModelMain>
 {
     private bool gridLinesEnabled;
 
@@ -13,7 +13,7 @@ internal class ViewportViewModel : SubViewModel<ViewModelMain>
         set => SetProperty(ref gridLinesEnabled, value);
     }
 
-    public ViewportViewModel(ViewModelMain owner)
+    public ViewOptionsViewModel(ViewModelMain owner)
         : base(owner)
     {
     }
@@ -28,7 +28,9 @@ internal class ViewportViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.View.Zoomout", -1, "Zoom out", "Zoom out", CanExecute = "PixiEditor.HasDocument", Key = Key.OemMinus)]
     public void ZoomViewport(double zoom)
     {
-        Document.DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
-        doc?.ZoomViewportTrigger.Execute(this, zoom);
+        ViewportWindowViewModel? viewport = Owner.WindowSubViewModel.ActiveWindow as ViewportWindowViewModel;
+        if (viewport is null)
+            return;
+        viewport.ZoomViewportTrigger.Execute(this, zoom);
     }
 }

+ 85 - 3
src/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs

@@ -1,22 +1,41 @@
-using System.Windows.Input;
+using System.Collections.ObjectModel;
+using System.Windows.Input;
 using AvalonDock.Layout;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands;
+using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 using Command = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
+#nullable enable
 [Command.Group("PixiEditor.Window", "Windows")]
 internal class WindowViewModel : SubViewModel<ViewModelMain>
 {
     private CommandController commandController;
-    private ShortcutPopup shortcutPopup;
-
+    private ShortcutPopup? shortcutPopup;
     private ShortcutPopup ShortcutPopup => shortcutPopup ?? (shortcutPopup = new(commandController));
 
     public RelayCommand<string> ShowAvalonDockWindowCommand { get; set; }
+    public ObservableCollection<ViewportWindowViewModel> Viewports { get; } = new();
+    public event EventHandler<ViewportWindowViewModel>? ActiveViewportChanged;
+
+    private object? activeWindow;
+    public object? ActiveWindow
+    {
+        get => activeWindow;
+        set
+        {
+            if (activeWindow == value)
+                return;
+            activeWindow = value;
+            RaisePropertyChanged(nameof(ActiveWindow));
+            if (activeWindow is ViewportWindowViewModel viewport)
+                ActiveViewportChanged?.Invoke(this, viewport);
+        }
+    }
 
     public WindowViewModel(ViewModelMain owner, CommandController commandController)
         : base(owner)
@@ -25,6 +44,69 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
         this.commandController = commandController;
     }
 
+    [Command.Basic("PixiEditor.Window.CreateNewViewport", "New window for current image", "New window for current image", CanExecute = "PixiEditor.HasDocument")]
+    public void CreateNewViewport()
+    {
+        var doc = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+        CreateNewViewport(doc);
+    }
+
+    public void CreateNewViewport(DocumentViewModel doc)
+    {
+        Viewports.Add(new ViewportWindowViewModel(doc));
+        foreach (var viewport in Viewports.Where(vp => vp.Document == doc))
+        {
+            viewport.RaisePropertyChanged(nameof(viewport.Index));
+        }
+    }
+
+    public void MakeDocumentViewportActive(DocumentViewModel? doc)
+    {
+        if (doc is null)
+        {
+            ActiveWindow = null;
+            Owner.DocumentManagerSubViewModel.MakeActiveDocumentNull();
+            return;
+        }
+        ActiveWindow = Viewports.Where(viewport => viewport.Document == doc).FirstOrDefault();
+    }
+
+    public string CalculateViewportIndex(ViewportWindowViewModel viewport)
+    {
+        ViewportWindowViewModel[] viewports = Viewports.Where(a => a.Document == viewport.Document).ToArray();
+        if (viewports.Length < 2)
+            return "";
+        return $"[{Array.IndexOf(viewports, viewport) + 1}]";
+    }
+
+    public void OnViewportWindowCloseButtonPressed(ViewportWindowViewModel viewport)
+    {
+        var viewports = Viewports.Where(vp => vp.Document == viewport.Document).ToArray();
+        if (viewports.Length == 1)
+        {
+            Owner.DisposeDocumentWithSaveConfirmation(viewport.Document);
+        }
+        else
+        {
+            Viewports.Remove(viewport);
+            foreach (var sibling in viewports)
+            {
+                sibling.RaisePropertyChanged(nameof(sibling.Index));
+            }
+        }
+    }
+
+    public void CloseViewportsForDocument(DocumentViewModel document)
+    {
+        var viewports = Viewports.Where(vp => vp.Document == document).ToArray();
+        foreach (ViewportWindowViewModel viewport in viewports)
+        {
+            Viewports.Remove(viewport);
+        }
+    }
+
     [Command.Basic("PixiEditor.Window.OpenSettingsWindow", "Open Settings", "Open Settings Window", Key = Key.OemComma, Modifiers = ModifierKeys.Control)]
     public static void OpenSettingsWindow(string page)
     {

+ 55 - 50
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -4,6 +4,8 @@ using PixiEditor.Helpers;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.Events;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -44,7 +46,7 @@ internal class ViewModelMain : ViewModelBase
 
     public SelectionViewModel SelectionSubViewModel { get; set; }
 
-    public ViewportViewModel ViewportSubViewModel { get; set; }
+    public ViewOptionsViewModel ViewportSubViewModel { get; set; }
 
     public ColorsViewModel ColorsSubViewModel { get; set; }
 
@@ -112,6 +114,7 @@ internal class ViewModelMain : ViewModelBase
         Preferences = services.GetRequiredService<IPreferences>();
 
         Preferences.Init();
+        WindowSubViewModel = services.GetService<WindowViewModel>();
         DocumentManagerSubViewModel = services.GetRequiredService<DocumentManagerViewModel>();
         SelectionSubViewModel = services.GetService<SelectionViewModel>();
 
@@ -126,7 +129,7 @@ internal class ViewModelMain : ViewModelBase
         LayersSubViewModel = services.GetService<LayersViewModel>();
         ClipboardSubViewModel = services.GetService<ClipboardViewModel>();
         UndoSubViewModel = services.GetService<UndoViewModel>();
-        ViewportSubViewModel = services.GetService<ViewportViewModel>();
+        ViewportSubViewModel = services.GetService<ViewOptionsViewModel>();
         ColorsSubViewModel = services.GetService<ColorsViewModel>();
         ColorsSubViewModel?.SetupPaletteParsers(services);
 
@@ -136,7 +139,6 @@ internal class ViewModelMain : ViewModelBase
         UpdateSubViewModel = services.GetService<UpdateViewModel>();
         DebugSubViewModel = services.GetService<DebugViewModel>();
 
-        WindowSubViewModel = services.GetService<WindowViewModel>();
         StylusSubViewModel = services.GetService<StylusViewModel>();
         RegistrySubViewModel = services.GetService<RegistryViewModel>();
 
@@ -149,6 +151,8 @@ internal class ViewModelMain : ViewModelBase
         ToolsSubViewModel?.SetupToolsTooltipShortcuts(services);
 
         SearchSubViewModel = services.GetService<SearchViewModel>();
+
+        DocumentManagerSubViewModel.ActiveDocumentChanged += OnActiveDocumentChanged;
     }
 
     public bool DocumentIsNotNull(object property)
@@ -168,7 +172,7 @@ internal class ViewModelMain : ViewModelBase
             throw new ArgumentException();
         }
 
-        ((CancelEventArgs)property).Cancel = !RemoveDocumentsWithSaveConfirmation();
+        ((CancelEventArgs)property).Cancel = !DisposeAllDocumentsWithSaveConfirmation();
     }
 
     private void ToolsSubViewModel_SelectedToolChanged(object sender, SelectedToolEventArgs e)
@@ -178,8 +182,6 @@ internal class ViewModelMain : ViewModelBase
         e.NewTool.PropertyChanged += SelectedTool_PropertyChanged;
 
         NotifyToolActionDisplayChanged();
-
-        //BitmapManager.InputTarget.OnToolChange(e.NewTool);
     }
 
     private void SelectedTool_PropertyChanged(object sender, PropertyChangedEventArgs e)
@@ -196,17 +198,16 @@ internal class ViewModelMain : ViewModelBase
     }
 
     /// <summary>
-    /// Removes documents with unsaved changes confirmation dialog.
+    /// Closes documents with unsaved changes confirmation dialog.
     /// </summary>
     /// <returns>If documents was removed successfully.</returns>
-    private bool RemoveDocumentsWithSaveConfirmation()
+    private bool DisposeAllDocumentsWithSaveConfirmation()
     {
-        /*
-        int docCount = BitmapManager.Documents.Count;
+        int docCount = DocumentManagerSubViewModel.Documents.Count;
         for (int i = 0; i < docCount; i++)
         {
-            BitmapManager.ActiveDocument = BitmapManager.Documents.First();
-            bool canceled = !RemoveDocumentWithSaveConfirmation();
+            WindowSubViewModel.MakeDocumentViewportActive(DocumentManagerSubViewModel.Documents.First());
+            bool canceled = !DisposeActiveDocumentWithSaveConfirmation();
             if (canceled)
             {
                 return false;
@@ -214,43 +215,58 @@ internal class ViewModelMain : ViewModelBase
         }
 
         return true;
-        */
-        return true;
     }
 
     /// <summary>
-    /// Removes document with unsaved changes confirmation dialog.
+    /// Disposes the active document after showing the unsaved changes confirmation dialog.
     /// </summary>
-    /// <returns>If document was removed successfully.</returns>
-    private bool RemoveDocumentWithSaveConfirmation()
+    /// <returns>If the document was closed successfully.</returns>
+    public bool DisposeActiveDocumentWithSaveConfirmation()
     {
-        /*
-        ConfirmationType result = ConfirmationType.No;
+        if (DocumentManagerSubViewModel.ActiveDocument is null)
+            return false;
+        return DisposeDocumentWithSaveConfirmation(DocumentManagerSubViewModel.ActiveDocument);
+    }
 
-        if (!BitmapManager.ActiveDocument.ChangesSaved)
+    public bool DisposeDocumentWithSaveConfirmation(DocumentViewModel document)
+    {
+        const string ConfirmationDialogTitle = "Unsaved changes";
+        const string ConfirmationDialogMessage = "The document has been modified. Do you want to save changes?";
+
+        ConfirmationType result = ConfirmationType.No;
+        if (!document.AllChangesSaved)
         {
-            result = ConfirmationDialog.Show(DocumentViewModel.ConfirmationDialogMessage, DocumentViewModel.ConfirmationDialogTitle);
+            result = ConfirmationDialog.Show(ConfirmationDialogMessage, ConfirmationDialogTitle);
             if (result == ConfirmationType.Yes)
             {
-                FileSubViewModel.SaveDocument(false);
-                //cancel was pressed in the save file dialog
-                if (!BitmapManager.ActiveDocument.ChangesSaved)
+                if (!FileSubViewModel.SaveDocument(document, false))
                     return false;
             }
         }
 
         if (result != ConfirmationType.Canceled)
         {
-            var doc = BitmapManager.ActiveDocument;
-            BitmapManager.Documents.Remove(doc);
-            doc.Dispose();
+            DocumentManagerSubViewModel.Documents.Remove(document);
+            if (DocumentManagerSubViewModel.ActiveDocument == document)
+            {
+                if (DocumentManagerSubViewModel.Documents.Count > 0)
+                    WindowSubViewModel.MakeDocumentViewportActive(DocumentManagerSubViewModel.Documents.Last());
+                else
+                    WindowSubViewModel.MakeDocumentViewportActive(null);
+            }
+
+            // TODO: this thing should actually dispose the document to free up ram
+            // We need the UI to be able to handle disposed documents
+            // Like, the viewports should show nothing, the commands shouldn't work, etc. At least nothing should crash or behave unexpectidly
+            // Mostly we only care about this because avalondock doesn't remove the UI elements of closed viewports (at least not right away)
+            // So they remain alive and keep "showing" the now disposed DocumentViewModel
+            // And since they reference the DocumentViewModel it doesn't get collected by GC
+
+            // document.Dispose();
+            WindowSubViewModel.CloseViewportsForDocument(document);
 
             return true;
         }
-        else
-        {
-            return false;
-        }*/
         return false;
     }
 
@@ -259,30 +275,19 @@ internal class ViewModelMain : ViewModelBase
         OnStartupEvent?.Invoke(this, EventArgs.Empty);
     }
 
-    private void BitmapManager_DocumentChanged(object sender)
+    private void OnActiveDocumentChanged(object sender, DocumentChangedEventArgs e)
     {
-        /*
-        if (e.NewDocument != null)
-        {
-            e.NewDocument.DocumentSizeChanged += ActiveDocument_DocumentSizeChanged;
-        }*/
+        if (e.OldDocument is not null)
+            e.OldDocument.SizeChanged -= ActiveDocument_DocumentSizeChanged;
+        if (e.NewDocument is not null)
+            e.NewDocument.SizeChanged += ActiveDocument_DocumentSizeChanged;
     }
 
     private void ActiveDocument_DocumentSizeChanged(object sender, DocumentSizeChangedEventArgs e)
     {
-        //BitmapManager.ActiveDocument.ChangesSaved = false;
-        DocumentViewModel doc = DocumentManagerSubViewModel.ActiveDocument;
-        if (doc is null)
-            throw new InvalidOperationException();
-        doc.CenterViewportTrigger.Execute(this, doc.SizeBindable);
-    }
-
-    private void BitmapUtility_BitmapChanged(object sender, EventArgs e)
-    {
-        //BitmapManager.ActiveDocument.ChangesSaved = false;
-        /*if (ToolsSubViewModel.ActiveTool is BitmapOperationTool)
+        foreach (var viewport in WindowSubViewModel.Viewports.Where(viewport => viewport.Document == e.Document))
         {
-            ColorsSubViewModel.AddSwatch(ColorsSubViewModel.PrimaryColor);
-        }*/
+            viewport.CenterViewportTrigger.Execute(this, e.NewSize);
+        }
     }
 }

+ 23 - 0
src/PixiEditor/ViewModels/ViewportWindowViewModel.cs

@@ -0,0 +1,23 @@
+using ChunkyImageLib.DataHolders;
+using PixiEditor.Helpers;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.ViewModels;
+#nullable enable
+internal class ViewportWindowViewModel : NotifyableObject
+{
+    public DocumentViewModel Document { get; }
+
+    public ExecutionTrigger<VecI> CenterViewportTrigger { get; } = new ExecutionTrigger<VecI>();
+    public ExecutionTrigger<double> ZoomViewportTrigger { get; } = new ExecutionTrigger<double>();
+
+    public string Index => ViewModelMain.Current?.WindowSubViewModel.CalculateViewportIndex(this) ?? "";
+
+    public RelayCommand RequestCloseCommand { get; }
+
+    public ViewportWindowViewModel(DocumentViewModel document)
+    {
+        Document = document;
+        RequestCloseCommand = new RelayCommand(_ => ViewModelMain.Current?.WindowSubViewModel.OnViewportWindowCloseButtonPressed(this));
+    }
+}

+ 24 - 18
src/PixiEditor/Views/MainWindow.xaml

@@ -275,10 +275,9 @@
                     <MenuItem
                         Header="_View">
                         <MenuItem
-                            Header="_Show Grid Lines"
-                            IsChecked="{Binding ViewportSubViewModel.GridLinesEnabled, Mode=TwoWay}"
-                            IsCheckable="True"
-                            InputGestureText="{cmds:ShortcutBinding PixiEditor.View.ToggleGrid}" />
+                            Header="New window for current image"
+                            cmds:Menu.Command="PixiEditor.Window.CreateNewViewport" />
+                        <Separator/>
                         <MenuItem
                             Header="Open _Startup Window"
                             ToolTip="Hello there!"
@@ -287,8 +286,14 @@
                             Header="Open _Navigation Window"
                             cmds:Menu.Command="PixiEditor.Window.OpenNavigationWindow" />
                         <MenuItem
-                            Header="Open _Shortcuts Window"
+                            Header="Open Short_cuts Window"
                             cmds:Menu.Command="PixiEditor.Window.OpenShortcutWindow" />
+                        <Separator/>
+                        <MenuItem
+                            Header="Show _Grid Lines"
+                            IsChecked="{Binding ViewportSubViewModel.GridLinesEnabled, Mode=TwoWay}"
+                            IsCheckable="True"
+                            InputGestureText="{cmds:ShortcutBinding PixiEditor.View.ToggleGrid}" />
                     </MenuItem>
                     <MenuItem
                         Header="_Help">
@@ -487,8 +492,8 @@
             <Grid Grid.Column="1" Grid.Row="2" Background="#303030" >
                 <Grid AllowDrop="True" Drop="MainWindow_Drop">
                     <DockingManager 
-                        ActiveContent="{Binding DocumentManagerSubViewModel.ActiveWindow, Mode=TwoWay}"
-                        DocumentsSource="{Binding DocumentManagerSubViewModel.Documents}">
+                        ActiveContent="{Binding WindowSubViewModel.ActiveWindow, Mode=TwoWay}"
+                        DocumentsSource="{Binding WindowSubViewModel.Viewports}">
                         <DockingManager.Theme>
                             <avalonDockTheme:PixiEditorDockTheme />
                         </DockingManager.Theme>
@@ -501,13 +506,14 @@
                                             <Setter.Value>
                                                 <MultiBinding Converter="{converters:ConcatStringsConverter}" ConverterParameter=" ">
                                                     <MultiBinding.Bindings>
-                                                        <Binding Path="Model.FileName"/>
-                                                        <Binding Path="Model.AllChangesSaved" Converter="{converters:BoolToAsteriskConverter}"/>
+                                                        <Binding Path="Model.Document.FileName"/>
+                                                        <Binding Path="Model.Document.AllChangesSaved" Converter="{converters:BoolToAsteriskConverter}"/>
+                                                        <Binding Path="Model.Index"/>
                                                     </MultiBinding.Bindings>
                                                 </MultiBinding>
                                             </Setter.Value>
                                         </Setter>
-                                        <Setter Property="CloseCommand" Value="{Binding Model.RequestCloseDocumentCommand}" />
+                                        <Setter Property="CloseCommand" Value="{Binding Model.RequestCloseCommand}" />
                                     </Style>
                                 </ui:PanelsStyleSelector.DocumentTabStyle>
                             </ui:PanelsStyleSelector>
@@ -515,26 +521,26 @@
                         <DockingManager.LayoutItemTemplateSelector>
                             <ui:DocumentsTemplateSelector>
                                 <ui:DocumentsTemplateSelector.DocumentsViewTemplate>
-                                    <DataTemplate DataType="{x:Type doc:DocumentViewModel}">
+                                    <DataTemplate DataType="{x:Type vm:ViewportWindowViewModel}">
                                         <usercontrols:Viewport
                                             CenterViewportTrigger="{Binding CenterViewportTrigger}"
                                             ZoomViewportTrigger="{Binding ZoomViewportTrigger}"
                                             MouseDownCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.MouseDownCommand}"
                                             MouseMoveCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.MouseMoveCommand}"
                                             MouseUpCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.MouseUpCommand}"
-                                            MiddleMouseClickedCommand="{Binding ElementName=mainWindow, Path=DataContext.IoSubViewModel.PreviewMouseMiddleButtonCommand}"
-                                            Cursor="{Binding ElementName=mainWindow, Path=DataContext.ToolsSubViewModel.ToolCursor}"
-                                            GridLinesVisible="{Binding ElementName=mainWindow, Path=DataContext.ViewportSubViewModel.GridLinesEnabled}"
-                                            ZoomMode="{Binding ElementName=mainWindow, Path=DataContext.ToolsSubViewModel.ActiveTool, Converter={converters:ActiveToolToZoomModeConverter}}"
-                                            ZoomOutOnClick="{Binding ElementName=mainWindow, Path=DataContext.ToolsSubViewModel.ZoomTool.ZoomOutOnClick}"
-                                            UseTouchGestures="{Binding ElementName=mainWindow, Path=DataContext.StylusSubViewModel.UseTouchGestures}"
+                                            MiddleMouseClickedCommand="{Binding IoSubViewModel.PreviewMouseMiddleButtonCommand, Source={vm:MainVM}}"
+                                            Cursor="{Binding ToolsSubViewModel.ToolCursor, Source={vm:MainVM}}"
+                                            GridLinesVisible="{Binding ViewportSubViewModel.GridLinesEnabled, Source={vm:MainVM}}"
+                                            ZoomMode="{Binding ToolsSubViewModel.ActiveTool, Source={vm:MainVM}, Converter={converters:ActiveToolToZoomModeConverter}}"
+                                            ZoomOutOnClick="{Binding ToolsSubViewModel.ZoomTool.ZoomOutOnClick, Source={vm:MainVM}}"
+                                            UseTouchGestures="{Binding StylusSubViewModel.UseTouchGestures, Source={vm:MainVM}}"
                                             StylusButtonDownCommand="{cmds:Command PixiEditor.Stylus.StylusDown, UseProvided=True}"
                                             StylusButtonUpCommand="{cmds:Command PixiEditor.Stylus.StylusUp, UseProvided=True}"
                                             StylusGestureCommand="{cmds:Command PixiEditor.Stylus.StylusSystemGesture, UseProvided=True}"
                                             StylusOutOfRangeCommand="{cmds:Command PixiEditor.Stylus.StylusOutOfRange, UseProvided=True}"
                                             Stylus.IsTapFeedbackEnabled="False" 
                                             Stylus.IsTouchFeedbackEnabled="False"
-                                            Document="{Binding}">
+                                            Document="{Binding Document}">
                                             <usercontrols:Viewport.ContextMenu>
                                                 <ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
                                                     <ContextMenu.Template>

+ 1 - 1
src/PixiEditor/Views/UserControls/FixedViewport.xaml.cs

@@ -116,7 +116,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     {
         FixedViewport? viewport = ((FixedViewport)viewportObj);
         viewport.PropertyChanged?.Invoke(viewportObj, new(nameof(TargetBitmap)));
-        viewport.Document!.Operations.AddOrUpdateViewport(viewport.GetLocation());
+        viewport.Document?.Operations.AddOrUpdateViewport(viewport.GetLocation());
     }
 
     private void OnImageSizeChanged(object sender, SizeChangedEventArgs e)