From 4fa76b1af27ba0e8fc23bbf830a832d98a63d036 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Fri, 10 May 2019 23:23:28 -0500 Subject: [PATCH] Refactor ViewPort Part 4 Pull the completion code into a new type. This is actualy way better organized now. But a lot of code had to move, so I'm not confident that I didn't introduce any bugs. --- src/HexManiac.Core/HexManiac.Core.csproj | 1 + src/HexManiac.Core/Models/ModelDelta.cs | 8 + .../ViewModels/ChangeHistory.cs | 5 +- .../ViewModels/CompleteEditOperation.cs | 281 +++++++++++++ src/HexManiac.Core/ViewModels/ViewPort.cs | 369 ++++-------------- src/HexManiac.Tests/ChangeHistoryTests.cs | 1 + 6 files changed, 365 insertions(+), 300 deletions(-) create mode 100644 src/HexManiac.Core/ViewModels/CompleteEditOperation.cs diff --git a/src/HexManiac.Core/HexManiac.Core.csproj b/src/HexManiac.Core/HexManiac.Core.csproj index 387d239b..9c149018 100644 --- a/src/HexManiac.Core/HexManiac.Core.csproj +++ b/src/HexManiac.Core/HexManiac.Core.csproj @@ -54,6 +54,7 @@ + diff --git a/src/HexManiac.Core/Models/ModelDelta.cs b/src/HexManiac.Core/Models/ModelDelta.cs index 22f936a2..6894aee7 100644 --- a/src/HexManiac.Core/Models/ModelDelta.cs +++ b/src/HexManiac.Core/Models/ModelDelta.cs @@ -24,6 +24,14 @@ namespace HavenSoft.HexManiac.Core.Models { public event EventHandler OnNewDataChange; public bool HasDataChange { get; private set; } + public bool HasAnyChange => + HasDataChange || + addedRuns.Any() || + removedRuns.Any() || + addedNames.Any() || + removedNames.Any() || + addedUnmappedPointers.Any() || + removedUnmappedPointers.Any(); public int EarliestChange { get { diff --git a/src/HexManiac.Core/ViewModels/ChangeHistory.cs b/src/HexManiac.Core/ViewModels/ChangeHistory.cs index c5935c24..0de3770f 100644 --- a/src/HexManiac.Core/ViewModels/ChangeHistory.cs +++ b/src/HexManiac.Core/ViewModels/ChangeHistory.cs @@ -6,6 +6,7 @@ using System.Windows.Input; namespace HavenSoft.HexManiac.Core.ViewModels { public interface IChangeToken { bool HasDataChange { get; } + bool HasAnyChange { get; } event EventHandler OnNewDataChange; } @@ -79,7 +80,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { revert = revertChange; undo = new StubCommand { Execute = arg => UndoExecuted(), - CanExecute = arg => undoStack.Count > 0 || currentChange != null, + CanExecute = arg => undoStack.Count > 0 || (currentChange != null && currentChange.HasAnyChange), }; redo = new StubCommand { Execute = arg => RedoExecuted(), @@ -89,7 +90,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels { public void ChangeCompleted() { if (currentChange == null) return; + if (!currentChange.HasAnyChange) { currentChange = null; return; } VerifyRevertNotInProgress(); + undoStack.Push(currentChange); currentChange.OnNewDataChange -= OnCurrentTokenDataChanged; currentChange = null; diff --git a/src/HexManiac.Core/ViewModels/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/CompleteEditOperation.cs new file mode 100644 index 00000000..93fba446 --- /dev/null +++ b/src/HexManiac.Core/ViewModels/CompleteEditOperation.cs @@ -0,0 +1,281 @@ +using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.Models.Runs; +using HavenSoft.HexManiac.Core.ViewModels.DataFormats; +using System; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using static HavenSoft.HexManiac.Core.Models.Runs.PCSRun; +using static HavenSoft.HexManiac.Core.Models.Runs.PointerRun; + +namespace HavenSoft.HexManiac.Core.ViewModels { + internal class CompleteEditOperation : IDataFormatVisitor { + private readonly IDataModel Model; + private readonly int memoryLocation; + private readonly string CurrentText; + private readonly ModelDelta CurrentChange; + + public bool Result { get; private set; } // if true, the edit was completed correctly + public int NewDataIndex { get; private set; } // for completed edits, where should the selection move to? + public bool DataMoved { get; private set; } + public string MessageText { get; private set; } // is there a message to display to the user? For example, when data gets moved. + public string ErrorText { get; private set; } // is there an error to display to the user? For example, invalide pointer + public HexElement NewCell { get; private set; } // if result is true and this is not null, asign this one value back to the one cell + // and refresh the one cell (along with any other UnderEdit cells) + // if result is true and this _is_ null, then the entire screen needs to be refreshed. + + public CompleteEditOperation(IDataModel model, int memoryLocation, string currentText, ModelDelta currentChange) { + Model = model; + this.memoryLocation = memoryLocation; + CurrentText = currentText; + CurrentChange = currentChange; + + NewDataIndex = memoryLocation; + } + + public void Visit(Undefined dataFormat, byte data) => Visit((None)null, data); + + public void Visit(None dataFormat, byte data) { + if (CurrentText.StartsWith(PointerStart.ToString())) { + if (CurrentText.Last() != PointerEnd && CurrentText.Last() != ' ') return; + CompletePointerEdit(); + Result = true; + } else { + if (CurrentText.Length < 2) return; + CompleteHexEdit(CurrentText); + Result = true; + } + } + + public void Visit(UnderEdit dataFormat, byte data) => throw new NotImplementedException(); + + public void Visit(Pointer pointer, byte data) { + var run = Model.GetNextRun(memoryLocation); + if (run is ArrayRun && CurrentText[0] != PointerStart) { + ErrorText = "Pointers in tables cannot be removed without removing the table."; + return; + } + + Visit((None)null, data); + } + + public void Visit(Anchor anchor, byte data) { + anchor.OriginalFormat.Visit(this, data); + if (NewCell != null) NewCell = new HexElement(NewCell.Value, new Anchor(NewCell.Format, anchor.Name, anchor.Format, anchor.Sources)); + } + + public void Visit(PCS pcs, byte data) { + var currentText = CurrentText; + if (currentText.StartsWith(StringDelimeter.ToString())) currentText = currentText.Substring(1); + if (pcs.Position != 0 && CurrentText == StringDelimeter.ToString()) { + CompleteStringEdit(); + Result = true; + } else if (PCSString.PCS.Any(str => str == currentText)) { + CompleteCharacterEdit(pcs); + Result = true; + } + } + + public void Visit(EscapedPCS pcs, byte data) { + if (CurrentText.Length < 2) return; + CompleteCharacterEdit(pcs); + Result = true; + } + + public void Visit(ErrorPCS pcs, byte data) => throw new NotImplementedException(); + + public void Visit(Ascii ascii, byte data) { + CompleteAsciiEdit(ascii); + Result = true; + } + + public void Visit(Integer integer, byte data) { + if (char.IsWhiteSpace(CurrentText.Last())) { + CompleteIntegerEdit(integer); + Result = true; + } + } + + public void Visit(IntegerEnum integer, byte data) { + // must end in whitespace or must have matching quotation marks (ex. "Mr. Mime") + var quoteCount = CurrentText.Count(c => c == '"'); + + if (quoteCount == 0 && char.IsWhiteSpace(CurrentText.Last())) { + CompleteIntegerEnumEdit(integer); + Result = true; + } else if (quoteCount == 2) { + CompleteIntegerEnumEdit(integer); + Result = true; + } + } + + private void CompleteIntegerEdit(Integer integer) { + if (!int.TryParse(CurrentText, out var result)) { + ErrorText = $"Could not parse {CurrentText} as a number"; + return; + } + + var run = (ArrayRun)Model.GetNextRun(memoryLocation); + var offsets = run.ConvertByteOffsetToArrayOffset(memoryLocation); + int length = run.ElementContent[offsets.SegmentIndex].Length; + for (int i = 0; i < length; i++) { + CurrentChange.ChangeData(Model, offsets.SegmentStart + i, (byte)result); + result /= 0x100; + } + + if (result != 0) ErrorText = $"Warning: number was too big to fit in the available space."; + NewDataIndex = offsets.SegmentStart + length; + } + + private void CompleteIntegerEnumEdit(IntegerEnum integer) { + var array = (ArrayRun)Model.GetNextRun(memoryLocation); + var offsets = array.ConvertByteOffsetToArrayOffset(memoryLocation); + var segment = (ArrayRunEnumSegment)array.ElementContent[offsets.SegmentIndex]; + if (segment.TryParse(Model, CurrentText, out int value)) { + Model.WriteMultiByteValue(offsets.SegmentStart, segment.Length, CurrentChange, value); + NewDataIndex = offsets.SegmentStart + segment.Length; + } else { + ErrorText = $"Could not parse {CurrentText}as an enum from the {segment.EnumName} array"; + } + } + + private void CompleteAsciiEdit(Ascii asciiFormat) { + var content = (byte)CurrentText[0]; + + CurrentChange.ChangeData(Model, memoryLocation, content); + NewCell = new HexElement(content, new Ascii(asciiFormat.Source, asciiFormat.Position, CurrentText[0])); + NewDataIndex = memoryLocation + 1; + } + + private void CompletePointerEdit() { + // if they just started a pointer and then clicked off, there's nothing to complete + if (CurrentText == PointerStart + " ") return; + + var destination = CurrentText.Substring(1, CurrentText.Length - 2); + + if (destination.Length == 2 && destination.All(ViewPort.AllHexCharacters.Contains)) { CompleteHexEdit(destination); return; } + + Model.ExpandData(CurrentChange, memoryLocation + 3); + + var currentRun = Model.GetNextRun(memoryLocation); + bool inArray = currentRun.Start <= memoryLocation && currentRun is ArrayRun; + var sources = currentRun.PointerSources; + + if (!inArray) { + if (destination != string.Empty) { + Model.ClearFormatAndData(CurrentChange, memoryLocation, 4); + sources = null; + } else if (!(currentRun is NoInfoRun)) { + Model.ClearFormat(CurrentChange, memoryLocation, 4); + sources = null; + } + } + + int fullValue; + if (destination == string.Empty) { + fullValue = Model.ReadPointer(memoryLocation); + } else if (destination.All(ViewPort.AllHexCharacters.Contains) && destination.Length <= 7) { + while (destination.Length < 6) destination = "0" + destination; + fullValue = int.Parse(destination, NumberStyles.HexNumber); + } else { + fullValue = Model.GetAddressFromAnchor(CurrentChange, memoryLocation, destination); + } + + if (fullValue == Pointer.NULL || (0 <= fullValue && fullValue < Model.Count)) { + if (inArray) { + Model.UpdateArrayPointer(CurrentChange, memoryLocation, fullValue); + // Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged); + } else { + Model.WritePointer(CurrentChange, memoryLocation, fullValue); + Model.ObserveRunWritten(CurrentChange, new PointerRun(memoryLocation, sources)); + } + + NewDataIndex = memoryLocation + 4; + } else { + ErrorText = $"Address {fullValue.ToString("X2")} is not within the data."; + } + } + + private void CompleteStringEdit() { + int memoryLocation = this.memoryLocation; + + // all the bytes are already correct, just move to the next space + var run = Model.GetNextRun(memoryLocation); + if (run is PCSRun pcsRun) { + while (run.Start + run.Length > memoryLocation) { + CurrentChange.ChangeData(Model, memoryLocation, 0xFF); + memoryLocation++; + NewDataIndex = memoryLocation; + var newRunLength = PCSString.ReadString(Model, run.Start, true); + Model.ObserveRunWritten(CurrentChange, new PCSRun(run.Start, newRunLength, run.PointerSources)); + } + } else if (run is ArrayRun arrayRun) { + var offsets = arrayRun.ConvertByteOffsetToArrayOffset(memoryLocation); + CurrentChange.ChangeData(Model, memoryLocation, 0xFF); + memoryLocation++; + NewDataIndex = memoryLocation; + while (offsets.SegmentStart + arrayRun.ElementContent[offsets.SegmentIndex].Length > memoryLocation) { + CurrentChange.ChangeData(Model, memoryLocation, 0x00); + memoryLocation++; + NewDataIndex = memoryLocation; + } + } + } + + private void CompleteCharacterEdit(IDataFormat originalFormat) { + var editText = CurrentText; + if (editText.StartsWith("\"")) editText = editText.Substring(1); + var pcs = originalFormat as PCS; + var escaped = originalFormat as EscapedPCS; + var run = Model.GetNextRun(memoryLocation); + + var byteValue = escaped != null ? + byte.Parse(CurrentText, NumberStyles.HexNumber) : + (byte)Enumerable.Range(0, 0x100).First(i => PCSString.PCS[i] == editText); + + var position = pcs != null ? pcs.Position : escaped.Position; + HandleLastCharacterChange(memoryLocation, editText, pcs, run, position, byteValue); + } + + private void HandleLastCharacterChange(int memoryLocation, string editText, PCS pcs, IFormattedRun run, int position, byte byteValue) { + if (run is PCSRun) { + // if its the last character being edited on a normal string, try to expand + if (run.Length == position + 1) { + int extraBytesNeeded = editText == "\\\\" ? 2 : 1; + // last character edit: might require relocation + var newRun = Model.RelocateForExpansion(CurrentChange, run, run.Length + extraBytesNeeded); + if (newRun != run) { + MessageText = $"Text was automatically moved to {newRun.Start.ToString("X6")}. Pointers were updated."; + memoryLocation += newRun.Start - run.Start; + run = newRun; + DataMoved = true; + } + + CurrentChange.ChangeData(Model, memoryLocation + 1, 0xFF); + if (editText == "\\\\") CurrentChange.ChangeData(Model, memoryLocation + 2, 0xFF); + run = new PCSRun(run.Start, run.Length + extraBytesNeeded, run.PointerSources); + Model.ObserveRunWritten(CurrentChange, run); + } + } else if (run is ArrayRun arrayRun) { + // if the last characet is being edited for an array, truncate + var offsets = arrayRun.ConvertByteOffsetToArrayOffset(memoryLocation); + if (arrayRun.ElementContent[offsets.SegmentIndex].Length == position + 1) { + memoryLocation--; // move back one byte and edit that one instead + } + } else { + Debug.Fail("Why are we completing a character edit on something other than a PCSRun or an Array?"); + } + + CurrentChange.ChangeData(Model, memoryLocation, byteValue); + NewDataIndex = memoryLocation + 1; + } + + private void CompleteHexEdit(string currentText) { + var byteValue = byte.Parse(currentText, NumberStyles.HexNumber); + var run = Model.GetNextRun(memoryLocation); + if (!(run is NoInfoRun) || run.Start != memoryLocation) Model.ClearFormat(CurrentChange, memoryLocation, 1); + CurrentChange.ChangeData(Model, memoryLocation, byteValue); + NewDataIndex = memoryLocation + 1; + } + } +} diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index bc5af334..9dca4ed3 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -7,8 +7,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; -using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Windows.Input; @@ -1063,214 +1061,95 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var underEdit = element.Format as UnderEdit; if (underEdit == null) return false; // no edit to complete - if (underEdit.CurrentText.StartsWith(PointerStart.ToString())) { - if (!underEdit.CurrentText.EndsWith(PointerEnd.ToString()) && !underEdit.CurrentText.EndsWith(" ")) return false; - CompletePointerEdit(point); - RequestMenuClose?.Invoke(this, EventArgs.Empty); - return true; + if (TryGeneralCompleteEdit(underEdit.CurrentText, point, out bool result)) { + return result; } - if (underEdit.CurrentText.StartsWith(GotoMarker.ToString())) { - if (char.IsWhiteSpace(underEdit.CurrentText[underEdit.CurrentText.Length - 1])) { - var destination = underEdit.CurrentText.Substring(1); - ClearEdits(point); - Goto.Execute(destination); - RequestMenuClose?.Invoke(this, EventArgs.Empty); - return true; - } else { - return false; - } - } - if (underEdit.CurrentText.StartsWith(AnchorStart.ToString())) { - TryUpdate(ref anchorText, underEdit.CurrentText, nameof(AnchorText)); - if (!char.IsWhiteSpace(underEdit.CurrentText[underEdit.CurrentText.Length - 1])) { - AnchorTextVisible = true; - return false; - } - // only end the anchor edit if the [] brace count matches - if (underEdit.CurrentText.Sum(c => c == '[' ? 1 : c == ']' ? -1 : 0) != 0) { - AnchorTextVisible = true; - return false; - } - - if (!CompleteAnchorEdit(point)) exitEditEarly = true; - return true; - } + // normal case: whether or not to accept the edit depends on the existing cell format var dataIndex = scroll.ViewPointToDataIndex(point); - if (underEdit.CurrentText == ExtendArray.ToString() && Model.IsAtEndOfArray(dataIndex, out var arrayRun)) { - var originalArray = arrayRun; - var errorInfo = Model.CompleteArrayExtension(history.CurrentChange, ref arrayRun); - if (!errorInfo.HasError) { - if (arrayRun.Start != originalArray.Start) { - ScrollFromRunMove(arrayRun.Start + arrayRun.Length, arrayRun.Length, arrayRun); - } + var completeEditOperation = new CompleteEditOperation(Model, dataIndex, underEdit.CurrentText, history.CurrentChange); + underEdit.OriginalFormat.Visit(completeEditOperation, element.Value); + if (completeEditOperation.Result) { + if (completeEditOperation.NewCell != null) { + currentView[point.X, point.Y] = completeEditOperation.NewCell; + } + if (completeEditOperation.DataMoved || completeEditOperation.NewDataIndex > scroll.DataLength) scroll.DataLength = Model.Count; + if (!SilentScroll(completeEditOperation.NewDataIndex) && completeEditOperation.NewCell == null) { RefreshBackingData(); - } else { - OnError?.Invoke(this, errorInfo.ErrorMessage); } - return true; + var run = Model.GetNextRun(completeEditOperation.NewDataIndex); + if (completeEditOperation.DataMoved) UpdateToolsFromSelection(run.Start); + if (run is ArrayRun) Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged); + if (run is ArrayRun || run is PCSRun) Tools.Schedule(Tools.StringTool.DataForCurrentRunChanged); + if (completeEditOperation.MessageText != null) OnMessage?.Invoke(this, completeEditOperation.MessageText); + if (completeEditOperation.ErrorText != null) OnError?.Invoke(this, completeEditOperation.ErrorText); } - var originalFormat = underEdit.OriginalFormat; - if (originalFormat is Anchor) originalFormat = ((Anchor)originalFormat).OriginalFormat; - if (originalFormat is Ascii) { - CompleteAsciiEdit(point, underEdit.CurrentText); - return true; - } else if (originalFormat is PCS stringFormat) { - var currentText = underEdit.CurrentText; - if (currentText.StartsWith(StringDelimeter.ToString())) currentText = currentText.Substring(1); - if (stringFormat.Position != 0 && underEdit.CurrentText == StringDelimeter.ToString()) { - CompleteStringEdit(point); - return true; - } else if (PCSString.PCS.Any(str => str == currentText)) { - CompleteCharacterEdit(point); - return true; - } + return completeEditOperation.Result; + } + } - return false; - } else if (originalFormat is EscapedPCS escaped) { - if (underEdit.CurrentText.Length < 2) return false; - CompleteCharacterEdit(point); - return true; - } else if (originalFormat is IntegerEnum integerEnum) { - var currentText = underEdit.CurrentText; - - // must end in whitespace or must have matching quotation marks (ex. "Mr. Mime") - var quoteCount = currentText.Count(c => c == '"'); - if (quoteCount == 2) { - CompleteIntegerEnumEdit(point, currentText); - return true; - } else if (quoteCount == 0 && char.IsWhiteSpace(currentText.Last())) { - CompleteIntegerEnumEdit(point, currentText); - return true; - } - - return false; - } else if (originalFormat is Integer integer) { - var currentText = underEdit.CurrentText; - if (char.IsWhiteSpace(currentText.Last())) { - CompleteIntegerEdit(point, currentText); - return true; - } - return false; + /// + /// Some edits are valid no matter where you are in the data. + /// Try to complete one of those edits here. + /// Return true if it's a special edit. Result is true if the edit was completed. + /// + private bool TryGeneralCompleteEdit(string currentText, Point point, out bool result) { + // goto marker + if (currentText.StartsWith(GotoMarker.ToString())) { + if (char.IsWhiteSpace(currentText[currentText.Length - 1])) { + var destination = currentText.Substring(1); + ClearEdits(point); + Goto.Execute(destination); + RequestMenuClose?.Invoke(this, EventArgs.Empty); + result = true; + } else { + result = false; } - if (underEdit.CurrentText.Length < 2) return false; - CompleteHexEdit(point); return true; } - } - private void CompleteIntegerEdit(Point point, string currentText) { - var memoryLocation = scroll.ViewPointToDataIndex(point); - var editFormat = (UnderEdit)currentView[point.X, point.Y].Format; - - var integer = (Integer)(editFormat.OriginalFormat is Anchor anchor ? anchor.OriginalFormat : editFormat.OriginalFormat); - if (!int.TryParse(currentText, out var result)) { - OnError?.Invoke(this, $"Could not parse {currentText} as a number"); - return; - } - - var run = (ArrayRun)Model.GetNextRun(memoryLocation); - var offsets = run.ConvertByteOffsetToArrayOffset(memoryLocation); - int length = run.ElementContent[offsets.SegmentIndex].Length; - for (int i = 0; i < length; i++) { - history.CurrentChange.ChangeData(Model, offsets.SegmentStart + i, (byte)result); - result /= 0x100; - } - Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged); - if (result != 0) OnError?.Invoke(this, $"Warning: number was too big to fit in the available space."); - if (!SilentScroll(offsets.SegmentStart + length)) ClearEdits(point); - } - - private void CompleteIntegerEnumEdit(Point point, string currentText) { - var memoryLocation = scroll.ViewPointToDataIndex(point); - var array = (ArrayRun)Model.GetNextRun(memoryLocation); - var offsets = array.ConvertByteOffsetToArrayOffset(memoryLocation); - var segment = (ArrayRunEnumSegment)array.ElementContent[offsets.SegmentIndex]; - if (segment.TryParse(Model, currentText, out int value)) { - Model.WriteMultiByteValue(offsets.SegmentStart, segment.Length, history.CurrentChange, value); - Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged); - if (!SilentScroll(offsets.SegmentStart + segment.Length)) ClearEdits(point); - } else { - OnError?.Invoke(this, $"Could not parse {currentText}as an enum from the {segment.EnumName} array"); - ClearEdits(point); - } - } - - private void CompleteAsciiEdit(Point point, string currentText) { - var memoryLocation = scroll.ViewPointToDataIndex(point); - var editFormat = (UnderEdit)currentView[point.X, point.Y].Format; - var originalFormat = editFormat.OriginalFormat; - var asciiFormat = originalFormat as Ascii ?? (Ascii)((Anchor)originalFormat).OriginalFormat; - var content = (byte)currentText[0]; - - history.CurrentChange.ChangeData(Model, memoryLocation, content); - currentView[point.X, point.Y] = new HexElement(content, new Ascii(asciiFormat.Source, asciiFormat.Position, currentText[0])); - SilentScroll(memoryLocation + 1); - } - - private void CompletePointerEdit(Point point) { - var element = currentView[point.X, point.Y]; - var underEdit = (UnderEdit)element.Format; - - // if they just started a pointer and then clicked off, there's nothing to complete - if (underEdit.CurrentText == PointerStart + " ") { - ClearEdits(point); - return; - } - - var index = scroll.ViewPointToDataIndex(point); - var destination = underEdit.CurrentText.Substring(1, underEdit.CurrentText.Length - 2); - - if (destination.Length == 2 && destination.All(AllHexCharacters.Contains)) { - currentView[point.X, point.Y] = new HexElement(element.Value, new UnderEdit(underEdit.OriginalFormat, destination)); - CompleteHexEdit(point); - return; - } - - Model.ExpandData(history.CurrentChange, index + 3); - scroll.DataLength = Model.Count; - - var currentRun = Model.GetNextRun(index); - bool inArray = currentRun.Start <= index && currentRun is ArrayRun; - var sources = currentRun.PointerSources; - - if (!inArray) { - if (destination != string.Empty) { - Model.ClearFormatAndData(history.CurrentChange, index, 4); - sources = null; - } else if (!(currentRun is NoInfoRun)) { - Model.ClearFormat(history.CurrentChange, index, 4); - sources = null; + // anchor start + if (currentText.StartsWith(AnchorStart.ToString())) { + TryUpdate(ref anchorText, currentText, nameof(AnchorText)); + if (!char.IsWhiteSpace(currentText[currentText.Length - 1])) { + AnchorTextVisible = true; + result = false; + return true; } + + // only end the anchor edit if the [] brace count matches + if (currentText.Sum(c => c == '[' ? 1 : c == ']' ? -1 : 0) != 0) { + AnchorTextVisible = true; + result = false; + return true; + } + + if (!CompleteAnchorEdit(point)) exitEditEarly = true; + result = true; + return true; } - int fullValue; - if (destination == string.Empty) { - fullValue = Model.ReadPointer(index); - } else if (destination.All(AllHexCharacters.Contains) && destination.Length <= 7) { - while (destination.Length < 6) destination = "0" + destination; - fullValue = int.Parse(destination, NumberStyles.HexNumber); - } else { - fullValue = Model.GetAddressFromAnchor(history.CurrentChange, index, destination); - } - - if (fullValue == Pointer.NULL || (0 <= fullValue && fullValue < Model.Count)) { - if (inArray) { - Model.UpdateArrayPointer(history.CurrentChange, index, fullValue); - Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged); + // table extension + var dataIndex = scroll.ViewPointToDataIndex(point); + if (currentText == ExtendArray.ToString() && Model.IsAtEndOfArray(dataIndex, out var arrayRun)) { + var originalArray = arrayRun; + var errorInfo = Model.CompleteArrayExtension(history.CurrentChange, ref arrayRun); + if (!errorInfo.HasError) { + if (arrayRun.Start != originalArray.Start) { + ScrollFromRunMove(arrayRun.Start + arrayRun.Length, arrayRun.Length, arrayRun); + } + RefreshBackingData(); } else { - Model.WritePointer(history.CurrentChange, index, fullValue); - Model.ObserveRunWritten(history.CurrentChange, new PointerRun(index, sources)); + OnError?.Invoke(this, errorInfo.ErrorMessage); } - - ClearEdits(point); - SilentScroll(index + 4); - } else { - OnError?.Invoke(this, $"Address {fullValue.ToString("X2")} is not within the data."); - ClearEdits(point); + result = true; + return true; } + + result = default; + return false; } /// True if it was completed successfully, false if some sort of error occurred and we should abort the remainder of the edit. @@ -1308,100 +1187,6 @@ namespace HavenSoft.HexManiac.Core.ViewModels { return errorInfo.IsWarning; } - private void CompleteStringEdit(Point point) { - // all the bytes are already correct, just move to the next space - ClearEdits(point); - var memoryLocation = scroll.ViewPointToDataIndex(point); - var run = Model.GetNextRun(memoryLocation); - if (run is PCSRun pcsRun) { - while (run.Start + run.Length > memoryLocation) { - history.CurrentChange.ChangeData(Model, memoryLocation, 0xFF); - memoryLocation++; - Tools.Schedule(Tools.StringTool.DataForCurrentRunChanged); - SilentScroll(memoryLocation); - var newRunLength = PCSString.ReadString(Model, run.Start, true); - Model.ObserveRunWritten(history.CurrentChange, new PCSRun(run.Start, newRunLength, run.PointerSources)); - } - } else if (run is ArrayRun arrayRun) { - var offsets = arrayRun.ConvertByteOffsetToArrayOffset(memoryLocation); - history.CurrentChange.ChangeData(Model, memoryLocation, 0xFF); - memoryLocation++; - Tools.Schedule(Tools.StringTool.DataForCurrentRunChanged); - Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged); - SilentScroll(memoryLocation); - while (offsets.SegmentStart + arrayRun.ElementContent[offsets.SegmentIndex].Length > memoryLocation) { - history.CurrentChange.ChangeData(Model, memoryLocation, 0x00); - memoryLocation++; - SilentScroll(memoryLocation); - } - } - RefreshBackingData(); - } - - private void CompleteCharacterEdit(Point point) { - var memoryLocation = scroll.ViewPointToDataIndex(point); - var element = currentView[point.X, point.Y]; - var underEdit = (UnderEdit)element.Format; - - var editText = underEdit.CurrentText; - if (editText.StartsWith("\"")) editText = editText.Substring(1); - var pcs = underEdit.OriginalFormat as PCS; - pcs = pcs ?? (underEdit.OriginalFormat as Anchor)?.OriginalFormat as PCS; - var escaped = underEdit.OriginalFormat as EscapedPCS; - escaped = escaped ?? (underEdit.OriginalFormat as Anchor)?.OriginalFormat as EscapedPCS; - var run = Model.GetNextRun(memoryLocation); - var byteValue = escaped != null ? - byte.Parse(underEdit.CurrentText, NumberStyles.HexNumber) : - (byte)Enumerable.Range(0, 0x100).First(i => PCSString.PCS[i] == editText); - - var position = pcs != null ? pcs.Position : escaped.Position; - HandleLastCharacterChange(ref memoryLocation, editText, pcs, ref run, position); - - history.CurrentChange.ChangeData(Model, memoryLocation, byteValue); - Tools.Schedule(Tools.StringTool.DataForCurrentRunChanged); - if (run is ArrayRun) Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged); - if (!SilentScroll(memoryLocation + 1)) { - point = scroll.DataIndexToViewPoint(memoryLocation); - RefreshBackingData(point); - if (point.X + 1 < Width) { - RefreshBackingData(new Point(point.X + 1, point.Y)); - } else { - RefreshBackingData(new Point(0, point.Y + 1)); - } - } - } - - private void HandleLastCharacterChange(ref int memoryLocation, string editText, PCS pcs, ref IFormattedRun run, int position) { - if (run is PCSRun) { - // if its the last character being edited on a normal string, try to expand - if (run.Length == position + 1) { - int extraBytesNeeded = editText == "\\\\" ? 2 : 1; - // last character edit: might require relocation - var newRun = Model.RelocateForExpansion(history.CurrentChange, run, run.Length + extraBytesNeeded); - if (newRun != run) { - OnMessage?.Invoke(this, $"Text was automatically moved to {newRun.Start.ToString("X6")}. Pointers were updated."); - ScrollFromRunMove(memoryLocation, pcs.Position, newRun); - memoryLocation += newRun.Start - run.Start; - run = newRun; - UpdateToolsFromSelection(run.Start); - } - - history.CurrentChange.ChangeData(Model, memoryLocation + 1, 0xFF); - if (editText == "\\\\") history.CurrentChange.ChangeData(Model, memoryLocation + 2, 0xFF); - run = new PCSRun(run.Start, run.Length + extraBytesNeeded, run.PointerSources); - Model.ObserveRunWritten(history.CurrentChange, run); - } - } else if (run is ArrayRun arrayRun) { - // if the last characet is being edited for an array, truncate - var offsets = arrayRun.ConvertByteOffsetToArrayOffset(memoryLocation); - if (arrayRun.ElementContent[offsets.SegmentIndex].Length == position + 1) { - memoryLocation--; // move back one byte and edit that one instead - } - } else { - Debug.Fail("Why are we completing a character edit on something other than a PCSRun or an Array?"); - } - } - private void ScrollFromRunMove(int originalIndexInData, int indexInOldRun, IFormattedRun newRun) { scroll.DataLength = Model.Count; // possible length change var offset = originalIndexInData - scroll.DataIndex; @@ -1410,20 +1195,6 @@ namespace HavenSoft.HexManiac.Core.ViewModels { selection.PropertyChanged += SelectionPropertyChanged; } - private void CompleteHexEdit(Point point) { - var element = currentView[point.X, point.Y]; - var underEdit = (UnderEdit)element.Format; - - var byteValue = byte.Parse(underEdit.CurrentText, NumberStyles.HexNumber); - var memoryLocation = scroll.ViewPointToDataIndex(point); - var run = Model.GetNextRun(memoryLocation); - if (!(run is NoInfoRun) || run.Start != memoryLocation) Model.ClearFormat(history.CurrentChange, memoryLocation, 1); - history.CurrentChange.ChangeData(Model, memoryLocation, byteValue); - scroll.DataLength = Model.Count; - ClearEdits(point); - SilentScroll(memoryLocation + 1); - } - private bool SilentScroll(int memoryLocation) { var nextPoint = scroll.DataIndexToViewPoint(memoryLocation); var didScroll = true; diff --git a/src/HexManiac.Tests/ChangeHistoryTests.cs b/src/HexManiac.Tests/ChangeHistoryTests.cs index 76f0c59a..4761b197 100644 --- a/src/HexManiac.Tests/ChangeHistoryTests.cs +++ b/src/HexManiac.Tests/ChangeHistoryTests.cs @@ -13,6 +13,7 @@ namespace HavenSoft.HexManiac.Tests { public event EventHandler OnNewDataChange; #pragma warning restore 0067 public bool HasDataChange => Count > 0; + public bool HasAnyChange => true; public FakeChangeToken() { } public FakeChangeToken(IEnumerable data) : base(data) { } }