mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-10-02 13:16:35 -05:00
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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user