diff --git a/src/HexManiac.Core/Models/ModelDelta.cs b/src/HexManiac.Core/Models/ModelDelta.cs index e8cd0e1e..59705b95 100644 --- a/src/HexManiac.Core/Models/ModelDelta.cs +++ b/src/HexManiac.Core/Models/ModelDelta.cs @@ -1,4 +1,6 @@ using HavenSoft.HexManiac.Core.Models.Runs; +using HavenSoft.HexManiac.Core.ViewModels; +using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +10,7 @@ namespace HavenSoft.HexManiac.Core.Models { /// editing data, adding / removing formats / format names, /// and tracking pointers that lead to a name that isn't in the ROM yet. /// - public class ModelDelta { + public class ModelDelta : IChangeToken { private readonly Dictionary oldData = new Dictionary(); private readonly Dictionary addedRuns = new Dictionary(); @@ -20,6 +22,9 @@ namespace HavenSoft.HexManiac.Core.Models { private readonly Dictionary addedUnmappedPointers = new Dictionary(); private readonly Dictionary removedUnmappedPointers = new Dictionary(); + public event EventHandler OnNewDataChange; + public bool HasDataChange { get; private set; } + public int EarliestChange { get { if (addedNames.Count > 0) return addedNames.Keys.Min(); @@ -50,6 +55,10 @@ namespace HavenSoft.HexManiac.Core.Models { } model[index] = data; + if (!HasDataChange) { + HasDataChange = true; + OnNewDataChange?.Invoke(this, EventArgs.Empty); + } } public void AddRun(IFormattedRun run) { @@ -89,7 +98,7 @@ namespace HavenSoft.HexManiac.Core.Models { } public ModelDelta Revert(IDataModel model) { - var reverse = new ModelDelta(); + var reverse = new ModelDelta { HasDataChange = HasDataChange }; foreach (var kvp in oldData) { var (index, data) = (kvp.Key, kvp.Value); diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index 7fb9102f..b7a056bc 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -426,16 +426,20 @@ namespace HavenSoft.HexManiac.Core.Models { public override void ObserveAnchorWritten(ModelDelta changeToken, string anchorName, IFormattedRun run) { int location = run.Start; int index = BinarySearch(location); - if (index < 0) { + + var existingRun = (index >= 0 && index < runs.Count) ? runs[index] : null; + + if (existingRun == null) { // no format starts exactly at this anchor, so clear any format that goes over this anchor. ClearFormat(changeToken, location, run.Length); } else if (!(run is NoInfoRun)) { - // a format starts exactly at this anchor, but this new format may extend further. Clear everything but the anchor. - ClearFormat(changeToken, run.Start, run.Length); + // a format starts exactly at this anchor. + // but the new format may extend further. If so, clear the existing format. + if (existingRun.Length < run.Length) { + ClearFormat(changeToken, run.Start, run.Length); + } } - var existingRun = (index >= 0 && index < runs.Count) ? runs[index] : null; - if (anchorForAddress.TryGetValue(location, out string oldAnchorName)) { anchorForAddress.Remove(location); addressForAnchor.Remove(oldAnchorName); diff --git a/src/HexManiac.Core/ViewModels/ChangeHistory.cs b/src/HexManiac.Core/ViewModels/ChangeHistory.cs index 3aa2ed73..c5935c24 100644 --- a/src/HexManiac.Core/ViewModels/ChangeHistory.cs +++ b/src/HexManiac.Core/ViewModels/ChangeHistory.cs @@ -4,6 +4,11 @@ using System.Runtime.CompilerServices; using System.Windows.Input; namespace HavenSoft.HexManiac.Core.ViewModels { + public interface IChangeToken { + bool HasDataChange { get; } + event EventHandler OnNewDataChange; + } + /// /// Represents a history of changes that can undo / redo. /// The change can be reperesented by any class with an empty constructor. @@ -17,7 +22,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { /// However, since ChangeHistory is not responsible for saving, you have to tell it whenever the data is saved. /// This is accomplished via the TagAsSaved() method. /// - public class ChangeHistory : ViewModelCore where T : class, new() { + public class ChangeHistory : ViewModelCore where T : class, IChangeToken, new() { private readonly Func revert; private readonly StubCommand undo, redo; private readonly Stack @@ -43,6 +48,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { if (currentChange == null) { bool notifyIsSavedChanged = IsSaved; currentChange = new T(); + currentChange.OnNewDataChange += OnCurrentTokenDataChanged; if (undoStack.Count == 0) undo.CanExecuteChanged.Invoke(undo, EventArgs.Empty); if (notifyIsSavedChanged) NotifyPropertyChanged(nameof(IsSaved)); } @@ -53,6 +59,22 @@ namespace HavenSoft.HexManiac.Core.ViewModels { public bool IsSaved => undoStackSizeAtSaveTag == undoStack.Count && currentChange == null; + public bool HasDataChange { + get { + if (IsSaved) return false; + var addedElements = undoStack.Count - undoStackSizeAtSaveTag; + var undoItems = undoStack.ToArray(); + var redoItems = redoStack.ToArray(); + for (int i = 0; i < addedElements; i++) { + if (undoItems[undoStackSizeAtSaveTag + i].HasDataChange) return true; + } + for (int i = 0; i < -addedElements; i++) { + if (redoItems[redoItems.Length - 1 - i].HasDataChange) return true; + } + return currentChange?.HasDataChange ?? false; + } + } + public ChangeHistory(Func revertChange) { revert = revertChange; undo = new StubCommand { @@ -69,18 +91,26 @@ namespace HavenSoft.HexManiac.Core.ViewModels { if (currentChange == null) return; VerifyRevertNotInProgress(); undoStack.Push(currentChange); + currentChange.OnNewDataChange -= OnCurrentTokenDataChanged; currentChange = null; } public void TagAsSaved() { ChangeCompleted(); - TryUpdate(ref undoStackSizeAtSaveTag, undoStack.Count, nameof(IsSaved)); + if(TryUpdate(ref undoStackSizeAtSaveTag, undoStack.Count, nameof(IsSaved))) { + NotifyPropertyChanged(nameof(HasDataChange)); + } + } + + private void OnCurrentTokenDataChanged(object sender, EventArgs e) { + NotifyPropertyChanged(nameof(HasDataChange)); } private void UndoExecuted() { ChangeCompleted(); if (undoStack.Count == 0) return; bool previouslyWasSaved = IsSaved; + bool previouslyHadDataChanged = HasDataChange; using (CreateRevertScope()) { var originalChange = undoStack.Pop(); @@ -91,11 +121,13 @@ namespace HavenSoft.HexManiac.Core.ViewModels { } if (previouslyWasSaved != IsSaved) NotifyPropertyChanged(nameof(IsSaved)); + if (previouslyHadDataChanged != HasDataChange) NotifyPropertyChanged(nameof(HasDataChange)); } private void RedoExecuted() { if (redoStack.Count == 0) return; bool previouslyWasSaved = IsSaved; + bool previouslyHadDataChanged = HasDataChange; VerifyRevertNotInProgress(); using (CreateRevertScope()) { @@ -107,6 +139,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { } if (previouslyWasSaved != IsSaved) NotifyPropertyChanged(nameof(IsSaved)); + if (previouslyHadDataChanged != HasDataChange) NotifyPropertyChanged(nameof(HasDataChange)); } private void VerifyRevertNotInProgress([CallerMemberName]string caller = null) { diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index 1450aebd..cf24af4f 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -40,7 +40,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { get { var name = Path.GetFileNameWithoutExtension(FileName); if (string.IsNullOrEmpty(name)) name = "Untitled"; - if (!history.IsSaved) name += "*"; + if (history.HasDataChange) name += "*"; return name; } } @@ -192,9 +192,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels { } private void HistoryPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName != nameof(history.IsSaved)) return; - save.CanExecuteChanged.Invoke(save, EventArgs.Empty); - NotifyPropertyChanged(nameof(Name)); + if (e.PropertyName == nameof(history.IsSaved)) save.CanExecuteChanged.Invoke(save, EventArgs.Empty); + if (e.PropertyName == nameof(history.HasDataChange)) NotifyPropertyChanged(nameof(Name)); } #endregion diff --git a/src/HexManiac.Tests/ChangeHistoryTests.cs b/src/HexManiac.Tests/ChangeHistoryTests.cs index 0f6a3603..de2a2172 100644 --- a/src/HexManiac.Tests/ChangeHistoryTests.cs +++ b/src/HexManiac.Tests/ChangeHistoryTests.cs @@ -8,17 +8,24 @@ using System.Linq; using Xunit; namespace HavenSoft.HexManiac.Tests { + public class FakeChangeToken : List, IChangeToken { + public event EventHandler OnNewDataChange; + public bool HasDataChange => Count > 0; + public FakeChangeToken() { } + public FakeChangeToken(IEnumerable data) : base(data) { } + } + public class ChangeHistoryTests { - private readonly ChangeHistory> history; + private readonly ChangeHistory history; private int callCount = 0; private List recentChanges; public ChangeHistoryTests() { - history = new ChangeHistory>(changes => { + history = new ChangeHistory(changes => { callCount++; recentChanges = changes; - return changes.Select(i => -i).ToList(); + return new FakeChangeToken(changes.Select(i => -i)); }); } @@ -146,8 +153,8 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void ThrowExceptionIfChangeStartsDuringUndo() { // setup a revert call that tries to access a change during revert - ChangeHistory history = null; - history = new ChangeHistory(token => history.CurrentChange.ToString()); + ChangeHistory history = null; + history = new ChangeHistory(token => history.CurrentChange); history.CurrentChange.ToString(); // create current change Assert.Throws(() => history.Undo.Execute()); @@ -155,11 +162,11 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void ThrowExceptionIfChangeCompletedDuringUndo() { - ChangeHistory history = null; - history = new ChangeHistory(token => { + ChangeHistory history = null; + history = new ChangeHistory(token => { history.CurrentChange.ToString(); history.ChangeCompleted(); - return new object(); + return new FakeChangeToken(); }); history.CurrentChange.ToString(); // create current change diff --git a/src/HexManiac.Tests/ViewPortSaveTests.cs b/src/HexManiac.Tests/ViewPortSaveTests.cs index af517fa0..abcb739b 100644 --- a/src/HexManiac.Tests/ViewPortSaveTests.cs +++ b/src/HexManiac.Tests/ViewPortSaveTests.cs @@ -224,6 +224,7 @@ namespace HavenSoft.HexManiac.Tests { viewPort.PropertyChanged += (sender, e) => { if (e.PropertyName == nameof(viewPort.Name)) nameChangedCount++; }; viewPort.Edit("012345"); + Assert.Equal(1, nameChangedCount); viewPort.Save.Execute(fileSystem); Assert.Equal("newfile", viewPort.Name); @@ -315,5 +316,35 @@ namespace HavenSoft.HexManiac.Tests { Assert.Equal("bob", ((Anchor)viewPort2[0, 0].Format).Name); } + + [Fact] + public void FormattingChangesDoNotMakeFileDirty() { + var buffer = Enumerable.Repeat((byte)0xFF, 0x200).ToArray(); + var model = new PokemonModel(buffer); + var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + var fileSystem = new StubFileSystem(); + + viewPort.Edit("^bob "); + + Assert.True(viewPort.Save.CanExecute(fileSystem)); + Assert.DoesNotContain("*", viewPort.Name); + } + + [Fact] + public void UndoRedoRestoresSaveStar() { + var buffer = Enumerable.Repeat((byte)0xFF, 0x200).ToArray(); + var model = new PokemonModel(buffer); + var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + int nameChangedCount = 0; + viewPort.PropertyChanged += (sender, e) => { if (e.PropertyName == nameof(viewPort.Name)) nameChangedCount++; }; + var fileSystem = new StubFileSystem(); + + viewPort.Edit("AA"); // notify 1 -> adding the star + viewPort.Undo.Execute(); // notify 2 -> removing the star + viewPort.Redo.Execute(); // notify 3 -> re-adding the star + + Assert.Contains("*", viewPort.Name); + Assert.Equal(3, nameChangedCount); + } } }