Browse Source

Display document name and unsaved changes indicator

Equbuxu 3 years ago
parent
commit
e3b675edd2

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changes/Change.cs

@@ -2,6 +2,8 @@
 
 internal abstract class Change : IDisposable
 {
+    public Guid ChangeGuid { get; } = Guid.NewGuid();
+
     /// <summary>
     /// Checks if this change can be combined with the <paramref name="other"/> change. Returns false if not overridden
     /// </summary>

+ 21 - 0
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -14,6 +14,18 @@ public class DocumentChangeTracker : IDisposable
     public IReadOnlyDocument Document => document;
     public bool HasSavedUndo => undoStack.Any();
     public bool HasSavedRedo => redoStack.Any();
+    public Guid? LastChangeGuid
+    {
+        get
+        {
+            if (!undoStack.Any())
+                return null;
+            List<Change> list = undoStack.Peek();
+            if (list.Count == 0)
+                return null;
+            return list[^1].ChangeGuid;
+        }
+    }
 
     private UpdateableChange? activeUpdateableChange = null;
     private List<Change>? activePacket = null;
@@ -34,16 +46,22 @@ public class DocumentChangeTracker : IDisposable
         activeUpdateableChange?.Dispose();
 
         if (activePacket != null)
+        {
             foreach (var change in activePacket)
                 change.Dispose();
+        }
 
         foreach (var list in undoStack)
+        {
             foreach (var change in list)
                 change.Dispose();
+        }
 
         foreach (var list in redoStack)
+        {
             foreach (var change in list)
                 change.Dispose();
+        }
     }
 
     public DocumentChangeTracker()
@@ -58,8 +76,11 @@ public class DocumentChangeTracker : IDisposable
         activePacket.Add(change);
 
         foreach (var changesToDispose in redoStack)
+        {
             foreach (var changeToDispose in changesToDispose)
                 changeToDispose.Dispose();
+        }
+
         redoStack.Clear();
     }
 

+ 18 - 0
src/PixiEditor/Helpers/Converters/BoolToAsteriskConverter.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PixiEditor.Helpers.Converters;
+internal class BoolToAsteriskConverter : SingleInstanceConverter<BoolToAsteriskConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is not bool boolean)
+            return DependencyProperty.UnsetValue;
+        return boolean ? "" : "*";
+    }
+}

+ 35 - 0
src/PixiEditor/Helpers/Converters/ConcatStringsConverter.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers.Converters;
+internal class ConcatStringsConverter : SingleInstanceMultiValueConverter<ConcatStringsConverter>
+{
+    public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+    {
+        string separator = parameter is string str ? str : "";
+        string combined = "";
+        bool first = true;
+        foreach (var entry in values)
+        {
+            if (entry is not { })
+                continue;
+            string toConcat;
+            if (entry is string entryString)
+                toConcat = entryString;
+            else
+                toConcat = entry.ToString();
+            if (toConcat != "")
+            {
+                if (!first)
+                    combined += separator;
+                first = false;
+                combined += toConcat;
+            }
+        }
+        return combined;
+    }
+}

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

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

+ 6 - 1
src/PixiEditor/Models/DataHolders/Document/Document.Commands.cs

@@ -10,7 +10,12 @@ internal partial class Document
 
     private void SetRelayCommands()
     {
-        //RequestCloseDocumentCommand = new RelayCommand(RequestCloseDocument);
+        RequestCloseDocumentCommand = new RelayCommand(RequestCloseDocument);
         //SetAsActiveOnClickCommand = new RelayCommand(SetAsActiveOnClick);
     }
+
+    private void RequestCloseDocument(object obj)
+    {
+        
+    }
 }

+ 6 - 4
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -68,10 +68,13 @@ internal class ActionAccumulator
                 changes = await helpers.Tracker.ProcessActions(toExecute);
 
             // update viewmodels based on changes
+            bool undoBoundaryPassed = toExecute.Any(static action => action is ChangeBoundary_Action or Redo_Action or Undo_Action);
             foreach (IChangeInfo? info in changes)
             {
                 helpers.Updater.ApplyChangeFromChangeInfo(info);
             }
+            if (undoBoundaryPassed)
+                helpers.Updater.AfterUndoBoundaryPassed();
 
             // render changes
             // If you are a sane person or maybe just someone who reads WPF documentation, you might think that the reasonable order of operations should be
@@ -94,15 +97,14 @@ internal class ActionAccumulator
 
             // update the contents of the bitmaps
             var affectedChunks = new AffectedChunkGatherer(helpers.Tracker, changes);
-            bool refreshDelayed = toExecute.Any(static action => action is ChangeBoundary_Action or Redo_Action or Undo_Action);
-            var renderResult = await renderer.UpdateGatheredChunks(affectedChunks, refreshDelayed);
+            var renderResult = await renderer.UpdateGatheredChunks(affectedChunks, undoBoundaryPassed);
             
             // lock bitmaps
             foreach (var (_, bitmap) in document.Bitmaps)
             {
                 bitmap.Lock();
             }
-            if (refreshDelayed)
+            if (undoBoundaryPassed)
                 LockPreviewBitmaps(document.StructureRoot);
 
             // add dirty rectangles
@@ -113,7 +115,7 @@ internal class ActionAccumulator
             {
                 bitmap.Unlock();
             }
-            if (refreshDelayed)
+            if (undoBoundaryPassed)
                 UnlockPreviewBitmaps(document.StructureRoot);
 
             // force refresh viewports for better responsiveness

+ 9 - 0
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -1,6 +1,7 @@
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
@@ -24,6 +25,14 @@ internal class DocumentUpdater
         this.helper = helper;
     }
 
+    /// <summary>
+    /// Don't call this outside ActionAccumulator
+    /// </summary>
+    public void AfterUndoBoundaryPassed()
+    {
+        doc.RaisePropertyChanged(nameof(doc.AllChangesSaved));
+    }
+
     /// <summary>
     /// Don't call this outside ActionAccumulator
     /// </summary>

+ 85 - 86
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -1,4 +1,5 @@
-using System.Windows.Input;
+using System.IO;
+using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using ChunkyImageLib;
@@ -20,72 +21,80 @@ namespace PixiEditor.ViewModels.SubViewModels.Document;
 #nullable enable
 internal class DocumentViewModel : NotifyableObject
 {
-    public const string ConfirmationDialogTitle = "Unsaved changes";
-    public const string ConfirmationDialogMessage = "The document has been modified. Do you want to save changes?";
-
+    private bool busy = false;
     public bool Busy
     {
         get => busy;
+        set => SetProperty(ref busy, value);
+    }
+
+    private string coordinatesString = "";
+    public string CoordinatesString
+    {
+        get => coordinatesString;
+        set => SetProperty(ref coordinatesString, value);
+    }
+
+    private string? fullFilePath = null;
+    public string? FullFilePath 
+    { 
+        get => fullFilePath;
         set
         {
-            busy = value;
-            RaisePropertyChanged(nameof(Busy));
+            SetProperty(ref fullFilePath, value);
+            RaisePropertyChanged(nameof(FileName));
         }
     }
-
-    public bool UpdateableChangeActive => Helpers.ChangeController.IsChangeActive;
-    public bool HasSavedUndo => Helpers.Tracker.HasSavedUndo;
-    public bool HasSavedRedo => Helpers.Tracker.HasSavedRedo;
-
-    public FolderViewModel StructureRoot { get; }
-
-    public DocumentStructureViewModel StructureViewModel { get; }
-
-    public int Width => size.X;
-    public int Height => size.Y;
-
-    public StructureMemberViewModel? SelectedStructureMember { get; private set; } = null;
-
-    private HashSet<StructureMemberViewModel> softSelectedStructureMembers = new();
-    public IReadOnlyCollection<StructureMemberViewModel> SoftSelectedStructureMembers => softSelectedStructureMembers;
-
-    public Dictionary<ChunkResolution, WriteableBitmap> Bitmaps { get; set; } = new()
+    public string FileName
     {
-        [ChunkResolution.Full] = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null),
-        [ChunkResolution.Half] = new WriteableBitmap(32, 32, 96, 96, PixelFormats.Pbgra32, null),
-        [ChunkResolution.Quarter] = new WriteableBitmap(16, 16, 96, 96, PixelFormats.Pbgra32, null),
-        [ChunkResolution.Eighth] = new WriteableBitmap(8, 8, 96, 96, PixelFormats.Pbgra32, null),
-    };
-
-    public WriteableBitmap PreviewBitmap { get; set; }
-    public SKSurface PreviewSurface { get; set; }
-    public string? FullFilePath { get; set; }
-
-    public Dictionary<ChunkResolution, SKSurface> Surfaces { get; set; } = new();
-
-    public VecI SizeBindable => size;
+        get => fullFilePath is null ? "Unnamed" : Path.GetFileName(fullFilePath);
+    }
 
-    public int HorizontalSymmetryAxisYBindable => horizontalSymmetryAxisY;
-    public int VerticalSymmetryAxisXBindable => verticalSymmetryAxisX;
+    private Guid? lastChangeOnSave = null;
+    public bool AllChangesSaved
+    {
+        get
+        {
+            return Helpers.Tracker.LastChangeGuid == lastChangeOnSave;
+        }
+    }
 
+    private bool horizontalSymmetryAxisEnabled;
     public bool HorizontalSymmetryAxisEnabledBindable
     {
         get => horizontalSymmetryAxisEnabled;
         set => Helpers.ActionAccumulator.AddFinishedActions(new SymmetryAxisState_Action(SymmetryAxisDirection.Horizontal, value));
     }
 
+    private bool verticalSymmetryAxisEnabled;
     public bool VerticalSymmetryAxisEnabledBindable
     {
         get => verticalSymmetryAxisEnabled;
         set => Helpers.ActionAccumulator.AddFinishedActions(new SymmetryAxisState_Action(SymmetryAxisDirection.Vertical, value));
     }
 
-    public IReadOnlyReferenceLayer? ReferenceLayer => Helpers.Tracker.Document.ReferenceLayer;
+    private VecI size = new VecI(64, 64);
+    public int Width => size.X;
+    public int Height => size.Y;
+    public VecI SizeBindable => size;
+
+    private int horizontalSymmetryAxisY;
+    public int HorizontalSymmetryAxisYBindable => horizontalSymmetryAxisY;
+
+    private int verticalSymmetryAxisX;
+    public int VerticalSymmetryAxisXBindable => verticalSymmetryAxisX;
+
+    private HashSet<StructureMemberViewModel> softSelectedStructureMembers = new();
+    public IReadOnlyCollection<StructureMemberViewModel> SoftSelectedStructureMembers => softSelectedStructureMembers;
 
+
+    public bool UpdateableChangeActive => Helpers.ChangeController.IsChangeActive;
+    public bool HasSavedUndo => Helpers.Tracker.HasSavedUndo;
+    public bool HasSavedRedo => Helpers.Tracker.HasSavedRedo;
+    public IReadOnlyReferenceLayer? ReferenceLayer => Helpers.Tracker.Document.ReferenceLayer;
     public BitmapSource? ReferenceBitmap => ReferenceLayer?.Image.ToWriteableBitmap();
     public VecI ReferenceBitmapSize => ReferenceLayer?.Image.Size ?? VecI.Zero;
     public ShapeCorners ReferenceShape => ReferenceLayer?.Shape ?? default;
-
     public Matrix ReferenceTransformMatrix
     {
         get
@@ -97,36 +106,33 @@ internal class DocumentViewModel : NotifyableObject
         }
     }
 
-    private string coordinatesString = "";
-    public string CoordinatesString
+    public FolderViewModel StructureRoot { get; }
+    public DocumentStructureViewModel StructureViewModel { get; }
+    public StructureMemberViewModel? SelectedStructureMember { get; private set; } = null;
+
+    public Dictionary<ChunkResolution, SKSurface> Surfaces { get; set; } = new();
+    public Dictionary<ChunkResolution, WriteableBitmap> Bitmaps { get; set; } = new()
     {
-        get => coordinatesString;
-        set => SetProperty(ref coordinatesString, value);
-    }
+        [ChunkResolution.Full] = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null),
+        [ChunkResolution.Half] = new WriteableBitmap(32, 32, 96, 96, PixelFormats.Pbgra32, null),
+        [ChunkResolution.Quarter] = new WriteableBitmap(16, 16, 96, 96, PixelFormats.Pbgra32, null),
+        [ChunkResolution.Eighth] = new WriteableBitmap(8, 8, 96, 96, PixelFormats.Pbgra32, null),
+    };
+    public WriteableBitmap PreviewBitmap { get; set; }
+    public SKSurface PreviewSurface { get; set; }
 
+
+    private SKPath selectionPath = new SKPath();
     public SKPath SelectionPathBindable => selectionPath;
-    public DocumentTransformViewModel TransformViewModel { get; }
+    
 
     public ExecutionTrigger<VecI> CenterViewportTrigger { get; } = new ExecutionTrigger<VecI>();
     public ExecutionTrigger<double> ZoomViewportTrigger { get; } = new ExecutionTrigger<double>();
+    public DocumentTransformViewModel TransformViewModel { get; }
 
 
     private DocumentHelpers Helpers { get; }
 
-    private int verticalSymmetryAxisX;
-
-    private bool horizontalSymmetryAxisEnabled;
-
-    private bool verticalSymmetryAxisEnabled;
-
-    private bool busy = false;
-
-    private VecI size = new VecI(64, 64);
-
-    private int horizontalSymmetryAxisY;
-
-    private SKPath selectionPath = new SKPath();
-
     public DocumentViewModel()
     {
         //Name = name;
@@ -137,29 +143,6 @@ internal class DocumentViewModel : NotifyableObject
         TransformViewModel = new();
         TransformViewModel.TransformMoved += (_, args) => Helpers.ChangeController.OnTransformMoved(args);
 
-        /*UndoCommand = new RelayCommand(Undo);
-        RedoCommand = new RelayCommand(Redo);
-        ClearSelectionCommand = new RelayCommand(ClearSelection);
-        CreateNewLayerCommand = new RelayCommand(_ => Helpers.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer));
-        CreateNewFolderCommand = new RelayCommand(_ => Helpers.StructureHelper.CreateNewStructureMember(StructureMemberType.Folder));
-        DeleteStructureMemberCommand = new RelayCommand(DeleteStructureMember);
-        ResizeCanvasCommand = new RelayCommand(ResizeCanvas);
-        ResizeImageCommand = new RelayCommand(ResizeImage);
-        CombineCommand = new RelayCommand(Combine);
-        ClearHistoryCommand = new RelayCommand(ClearHistory);
-        CreateMaskCommand = new RelayCommand(CreateMask);
-        DeleteMaskCommand = new RelayCommand(DeleteMask);
-        ToggleLockTransparencyCommand = new RelayCommand(ToggleLockTransparency);
-        PasteImageCommand = new RelayCommand(PasteImage);
-        CreateReferenceLayerCommand = new RelayCommand(CreateReferenceLayer);
-        ApplyTransformCommand = new RelayCommand(ApplyTransform);
-        DragSymmetryCommand = new RelayCommand(DragSymmetry);
-        EndDragSymmetryCommand = new RelayCommand(EndDragSymmetry);
-        ClipToMemberBelowCommand = new RelayCommand(ClipToMemberBelow);
-        ApplyMaskCommand = new RelayCommand(ApplyMask);
-        TransformSelectionPathCommand = new RelayCommand(TransformSelectionPath);
-        TransformSelectedAreaCommand = new RelayCommand(TransformSelectedArea);*/
-
         foreach (KeyValuePair<ChunkResolution, WriteableBitmap> bitmap in Bitmaps)
         {
             SKSurface? surface = SKSurface.Create(
@@ -240,6 +223,13 @@ internal class DocumentViewModel : NotifyableObject
 
     public void RemoveViewport(Guid viewportGuid) => Helpers.ActionAccumulator.AddActions(new RemoveViewport_PassthroughAction(viewportGuid));
 
+    public void ClearUndo()
+    {
+        if (Helpers.ChangeController.IsChangeActive)
+            return;
+        Helpers.ActionAccumulator.AddActions(new DeleteRecordedChanges_Action());
+    }
+
     public void CreateStructureMember(StructureMemberType type)
     {
         if (Helpers.ChangeController.IsChangeActive)
@@ -293,6 +283,7 @@ internal class DocumentViewModel : NotifyableObject
     public void UseOpacitySlider() => Helpers.ChangeController.TryStartUpdateableChange<StructureMemberOpacityExecutor>();
 
     public void UsePenTool() => Helpers.ChangeController.TryStartUpdateableChange<PenToolExecutor>();
+
     public void UseEllipseTool() => Helpers.ChangeController.TryStartUpdateableChange<EllipseToolExecutor>();
 
     public void Undo()
@@ -315,6 +306,7 @@ internal class DocumentViewModel : NotifyableObject
             return;
         Helpers.StructureHelper.TryMoveStructureMember(memberToMove, memberToMoveIntoOrNextTo, placement);
     }
+
     public void MergeStructureMembers(IReadOnlyList<Guid> members)
     {
         if (Helpers.ChangeController.IsChangeActive || members.Count < 2)
@@ -335,6 +327,12 @@ internal class DocumentViewModel : NotifyableObject
         Helpers.ActionAccumulator.AddActions(new ChangeBoundary_Action());
     }
 
+    public void MarkAsSaved()
+    {
+        lastChangeOnSave = Helpers.Tracker.LastChangeGuid;
+        RaisePropertyChanged(nameof(AllChangesSaved));
+    }
+
     public SKColor PickColor(VecI pos, bool fromAllLayers)
     {
         // there is a tiny chance that the image might get disposed by another thread
@@ -371,6 +369,8 @@ internal class DocumentViewModel : NotifyableObject
         }
     }
 
+
+    #region Events
     public void OnKeyDown(Key args) => Helpers.ChangeController.OnKeyDown(args);
     public void OnKeyUp(Key args) => Helpers.ChangeController.OnKeyUp(args);
 
@@ -381,9 +381,8 @@ internal class DocumentViewModel : NotifyableObject
         Helpers.ChangeController.OnMouseMove(newPos);
     }
     public void OnCanvasLeftMouseButtonUp() => Helpers.ChangeController.OnLeftMouseButtonUp();
-
     public void OnOpacitySliderDragStarted() => Helpers.ChangeController.OnOpacitySliderDragStarted();
     public void OnOpacitySliderDragged(float newValue) => Helpers.ChangeController.OnOpacitySliderDragged(newValue);
     public void OnOpacitySliderDragEnded() => Helpers.ChangeController.OnOpacitySliderDragEnded();
-
+    #endregion
 }

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

@@ -86,6 +86,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             doc.ResizeCanvas(size, ResizeAnchor.TopLeft);
         if (addBaseLayer)
             doc.CreateStructureMember(StructureMemberType.Layer);
+        doc.ClearUndo();
+        doc.MarkAsSaved();
 
         return doc;
     }

+ 10 - 2
src/PixiEditor/Views/MainWindow.xaml

@@ -15,7 +15,6 @@
     xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
     xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
     xmlns:ui="clr-namespace:PixiEditor.Helpers.UI"
-    xmlns:cmd="http://www.galasoft.ch/mvvmlight"
     xmlns:avalondock="https://github.com/Dirkster99/AvalonDock"
     xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
     xmlns:usercontrols="clr-namespace:PixiEditor.Views.UserControls"
@@ -491,7 +490,16 @@
                             <ui:PanelsStyleSelector>
                                 <ui:PanelsStyleSelector.DocumentTabStyle>
                                     <Style TargetType="{x:Type avalondock:LayoutItem}">
-                                        <Setter Property="Title" Value="{Binding Model.Name}" />
+                                        <Setter Property="Title">
+                                            <Setter.Value>
+                                                <MultiBinding Converter="{converters:ConcatStringsConverter}" ConverterParameter=" ">
+                                                    <MultiBinding.Bindings>
+                                                        <Binding Path="Model.FileName"/>
+                                                        <Binding Path="Model.AllChangesSaved" Converter="{converters:BoolToAsteriskConverter}"/>
+                                                    </MultiBinding.Bindings>
+                                                </MultiBinding>
+                                            </Setter.Value>
+                                        </Setter>
                                         <Setter Property="CloseCommand" Value="{Binding Model.RequestCloseDocumentCommand}" />
                                     </Style>
                                 </ui:PanelsStyleSelector.DocumentTabStyle>