only show the '*' in the tab if there is a data change.

but allow saving even if there's only a format change.
This commit is contained in:
Benjamin Popp
2019-04-20 14:20:02 -05:00
parent 26ebacc028
commit 695ff9cdf8
6 changed files with 104 additions and 21 deletions

View File

@@ -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.
/// </summary>
public class ModelDelta {
public class ModelDelta : IChangeToken {
private readonly Dictionary<int, byte> oldData = new Dictionary<int, byte>();
private readonly Dictionary<int, IFormattedRun> addedRuns = new Dictionary<int, IFormattedRun>();
@@ -20,6 +22,9 @@ namespace HavenSoft.HexManiac.Core.Models {
private readonly Dictionary<int, string> addedUnmappedPointers = new Dictionary<int, string>();
private readonly Dictionary<int, string> removedUnmappedPointers = new Dictionary<int, string>();
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);

View File

@@ -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);

View File

@@ -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;
}
/// <summary>
/// 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.
/// </remarks>
public class ChangeHistory<T> : ViewModelCore where T : class, new() {
public class ChangeHistory<T> : ViewModelCore where T : class, IChangeToken, new() {
private readonly Func<T, T> revert;
private readonly StubCommand undo, redo;
private readonly Stack<T>
@@ -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<T, T> 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) {

View File

@@ -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

View File

@@ -8,17 +8,24 @@ using System.Linq;
using Xunit;
namespace HavenSoft.HexManiac.Tests {
public class FakeChangeToken : List<int>, IChangeToken {
public event EventHandler OnNewDataChange;
public bool HasDataChange => Count > 0;
public FakeChangeToken() { }
public FakeChangeToken(IEnumerable<int> data) : base(data) { }
}
public class ChangeHistoryTests {
private readonly ChangeHistory<List<int>> history;
private readonly ChangeHistory<FakeChangeToken> history;
private int callCount = 0;
private List<int> recentChanges;
public ChangeHistoryTests() {
history = new ChangeHistory<List<int>>(changes => {
history = new ChangeHistory<FakeChangeToken>(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<object> history = null;
history = new ChangeHistory<object>(token => history.CurrentChange.ToString());
ChangeHistory<FakeChangeToken> history = null;
history = new ChangeHistory<FakeChangeToken>(token => history.CurrentChange);
history.CurrentChange.ToString(); // create current change
Assert.Throws<InvalidOperationException>(() => history.Undo.Execute());
@@ -155,11 +162,11 @@ namespace HavenSoft.HexManiac.Tests {
[Fact]
public void ThrowExceptionIfChangeCompletedDuringUndo() {
ChangeHistory<object> history = null;
history = new ChangeHistory<object>(token => {
ChangeHistory<FakeChangeToken> history = null;
history = new ChangeHistory<FakeChangeToken>(token => {
history.CurrentChange.ToString();
history.ChangeCompleted();
return new object();
return new FakeChangeToken();
});
history.CurrentChange.ToString(); // create current change

View File

@@ -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);
}
}
}