using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.Models.Runs.Factory;
using HavenSoft.HexManiac.Core.Models.Runs.Sprites;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Tools;
using HavenSoft.HexManiac.Core.ViewModels.Visitors;
using System;
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.Text;
using System.Windows.Input;
using static HavenSoft.HexManiac.Core.ICommandExtensions;
using static HavenSoft.HexManiac.Core.Models.Runs.ArrayRun;
using static HavenSoft.HexManiac.Core.Models.Runs.BaseRun;
using static HavenSoft.HexManiac.Core.Models.Runs.PCSRun;
using static HavenSoft.HexManiac.Core.Models.Runs.PointerRun;
namespace HavenSoft.HexManiac.Core.ViewModels {
///
/// A range of visible data that should be displayed.
///
public class ViewPort : ViewModelCore, IEditableViewPort, IRaiseMessageTab {
public const string AllHexCharacters = "0123456789ABCDEFabcdef";
public const char GotoMarker = '@';
public const char DirectiveMarker = '.'; // for things like .thumb, .align, etc. Directives always start with a single dot and contain no further dots until they contain a space.
public const char CommandMarker = '!'; // commands are meta, so they also start with the goto marker.
public const char CommentStart = '#';
public const int CopyLimit = 20000;
private static readonly NotifyCollectionChangedEventArgs ResetArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
private readonly StubCommand
undoWrapper = new StubCommand(),
redoWrapper = new StubCommand(),
clear = new StubCommand(),
selectAll = new StubCommand(),
copy = new StubCommand(),
copyAddress = new StubCommand(),
copyBytes = new StubCommand(),
deepCopy = new StubCommand(),
isText = new StubCommand();
public Singletons Singletons { get; }
private HexElement[,] currentView;
private bool exitEditEarly, withinComment, skipToNextGameCode;
public string Name {
get {
var name = Path.GetFileNameWithoutExtension(FileName);
if (string.IsNullOrEmpty(name)) name = "Untitled";
if (history.HasDataChange) name += "*";
return name;
}
}
private string fileName;
public string FileName {
get => fileName;
private set {
if (TryUpdate(ref fileName, value) && !string.IsNullOrEmpty(fileName)) {
FullFileName = Path.GetFullPath(fileName);
NotifyPropertyChanged(nameof(Name));
}
}
}
private string fullFileName;
public string FullFileName { get => fullFileName; private set => TryUpdate(ref fullFileName, value); }
#region Scrolling Properties
private readonly ScrollRegion scroll;
public event EventHandler PreviewScrollChanged;
public int Width {
get => scroll.Width;
set {
using (ModelCacheScope.CreateScope(Model)) selection.ChangeWidth(value);
}
}
public int Height {
get => scroll.Height;
set {
using (ModelCacheScope.CreateScope(Model)) scroll.Height = value;
}
}
public int MinimumScroll => scroll.MinimumScroll;
public int ScrollValue {
get => scroll.ScrollValue;
set {
PreviewScrollChanged?.Invoke(this, EventArgs.Empty);
using (ModelCacheScope.CreateScope(Model)) scroll.ScrollValue = value;
}
}
public int MaximumScroll => scroll.MaximumScroll;
public ObservableCollection Headers => scroll.Headers;
public ObservableCollection ColumnHeaders { get; }
public int DataOffset => scroll.DataIndex;
public ICommand Scroll => scroll.Scroll;
public bool UseCustomHeaders {
get => scroll.UseCustomHeaders;
set { using (ModelCacheScope.CreateScope(Model)) scroll.UseCustomHeaders = value; }
}
private void ScrollPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(scroll.DataIndex)) {
RefreshBackingData();
if (e is ExtendedPropertyChangedEventArgs ex) {
var previous = ex.OldValue;
if (Math.Abs(scroll.DataIndex - previous) % Width != 0) UpdateColumnHeaders();
}
} else if (e.PropertyName != nameof(scroll.DataLength)) {
NotifyPropertyChanged(e);
}
if (e.PropertyName == nameof(Width) || e.PropertyName == nameof(Height)) {
RefreshBackingData();
}
if (e.PropertyName == nameof(Width)) {
UpdateColumnHeaders();
NotifyPropertyChanged(nameof(ScrollValue)); // changing the Scroll's Width can mess with the ScrollValue: go ahead and notify
}
}
#endregion
#region Selection Properties
private readonly Selection selection;
private bool stretchData;
public bool StretchData { get => stretchData; set => Set(ref stretchData, value); }
public bool AutoAdjustDataWidth { get => selection.AutoAdjustDataWidth; set => selection.AutoAdjustDataWidth = value; }
public bool AllowMultipleElementsPerLine { get => selection.AllowMultipleElementsPerLine; set => selection.AllowMultipleElementsPerLine = value; }
public Point SelectionStart {
get => selection.SelectionStart;
set => selection.SelectionStart = value;
}
public Point SelectionEnd {
get => selection.SelectionEnd;
set => selection.SelectionEnd = value;
}
public int PreferredWidth {
get => selection.PreferredWidth;
set => selection.PreferredWidth = value;
}
private readonly StubCommand moveSelectionStart = new StubCommand(),
moveSelectionEnd = new StubCommand();
public ICommand MoveSelectionStart => moveSelectionStart;
public ICommand MoveSelectionEnd => moveSelectionEnd;
public ICommand Goto => StubCommand(ref gotoCommand, ExecuteGoto, selection.Goto.CanExecute);
public ICommand Back => selection.Back;
public ICommand Forward => selection.Forward;
public ICommand ResetAlignment => selection.ResetAlignment;
public ICommand SelectAll => selectAll;
private StubCommand gotoCommand;
private void ExecuteGoto(object arg) {
if (arg is string str) {
var words = Model.GetMatchedWords(str).Where(word => Model.GetNextRun(word).Length < 3).ToList();
if (words.Count == 1) {
selection.Goto.Execute(words[0]);
return;
} else if (words.Count > 1) {
OpenSearchResultsTab(str, words.Select(word => (word, word)).ToList());
return;
}
}
selection.Goto.Execute(arg);
}
private void ClearActiveEditBeforeSelectionChanges(object sender, Point location) {
if (location.X >= 0 && location.X < scroll.Width && location.Y >= 0 && location.Y < scroll.Height) {
var element = this[location.X, location.Y];
var underEdit = element.Format as UnderEdit;
if (underEdit != null) {
using (ModelCacheScope.CreateScope(Model)) {
if (underEdit.CurrentText == string.Empty) {
var index = scroll.ViewPointToDataIndex(location);
var operation = new DataClear(Model, history.CurrentChange, index);
underEdit.OriginalFormat.Visit(operation, Model[index]);
ClearEdits(location);
} else {
var endEdit = " ";
if (underEdit.CurrentText.Count(c => c == StringDelimeter) % 2 == 1) endEdit = StringDelimeter.ToString();
var originalFormat = underEdit.OriginalFormat;
if (originalFormat is Anchor anchor) originalFormat = anchor.OriginalFormat;
if (originalFormat is SpriteDecorator sprite) originalFormat = sprite.OriginalFormat;
if (underEdit.CurrentText.StartsWith(EggMoveRun.GroupStart) && (originalFormat is EggSection || originalFormat is EggItem)) endEdit = EggMoveRun.GroupEnd;
currentView[location.X, location.Y] = new HexElement(element.Value, element.Edited, underEdit.Edit(endEdit));
if (!TryCompleteEdit(location)) ClearEdits(location);
}
}
}
}
}
private void SelectionPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(SelectionEnd)) history.ChangeCompleted();
NotifyPropertyChanged(e.PropertyName);
var dataIndex = scroll.ViewPointToDataIndex(SelectionStart);
using (ModelCacheScope.CreateScope(Model)) {
UpdateToolsFromSelection(dataIndex);
UpdateSelectedAddress();
UpdateSelectedBytes();
}
}
public void UpdateToolsFromSelection(int dataIndex) {
var run = Model.GetNextRun(dataIndex);
if (run.Start > dataIndex) {
AnchorTextVisible = false;
return;
}
// if the user explicitly closed the tools, don't auto-open them.
if (tools.SelectedIndex != -1) {
// if the 'Raw' tool is selected, don't auto-update tool selection.
if (!(tools.SelectedTool == tools.CodeTool && tools.CodeTool.Mode == CodeMode.Raw)) {
using (ModelCacheScope.CreateScope(Model)) {
// update the tool from pointers too
if (run is PointerRun) {
var destination = Model.ReadPointer(run.Start);
run = Model.GetNextRun(destination);
dataIndex = destination;
}
if (run is ISpriteRun spriteRun) {
var tool = tools.SpriteTool;
if (tool.SpriteAddress != run.Start) {
tool.SpriteAddress = run.Start;
} else {
tool.UpdateSpriteProperties();
tool.PaletteAddress = SpriteTool.FindMatchingPalette(Model, spriteRun, tool.PaletteAddress);
}
tools.SelectedIndex = tools.IndexOf(tools.SpriteTool);
} else if (run is IPaletteRun) {
tools.SpriteTool.PaletteAddress = run.Start;
tools.SelectedIndex = tools.IndexOf(tools.SpriteTool);
} else if (run is ITableRun array) {
var offsets = array.ConvertByteOffsetToArrayOffset(dataIndex);
Tools.StringTool.Address = offsets.SegmentStart - offsets.ElementIndex * array.ElementLength;
Tools.TableTool.Address = array.Start + array.ElementLength * offsets.ElementIndex;
if (!(run is IStreamRun || array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.PCS) || tools.SelectedTool != tools.StringTool) {
tools.SelectedIndex = tools.IndexOf(tools.TableTool);
}
} else if (run is IStreamRun) {
Tools.StringTool.Address = run.Start;
tools.SelectedIndex = tools.IndexOf(tools.StringTool);
} else if (run is IScriptStartRun) {
tools.SelectedIndex = tools.IndexOf(tools.CodeTool);
if (run is XSERun) tools.CodeTool.Mode = CodeMode.Script;
if (run is BSERun) tools.CodeTool.Mode = CodeMode.BattleScript;
if (run is ASERun) tools.CodeTool.Mode = CodeMode.AnimationScript;
} else {
// not a special run, so don't update tools
}
}
}
}
if (this[SelectionStart].Format is Anchor anchor) {
// there is an anchor exactly here: show that format
TryUpdate(ref anchorText, AnchorStart + anchor.Name + anchor.Format, nameof(AnchorText));
AnchorTextVisible = true;
} else if (run.Start <= dataIndex && run.PointerSources != null) {
// there is an anchor attached to this run: show that format
var name = Model.GetAnchorFromAddress(-1, run.Start);
TryUpdate(ref anchorText, AnchorStart + name + run.FormatString, nameof(AnchorText));
AnchorTextVisible = true;
} else {
AnchorTextVisible = false;
}
RequestMenuClose?.Invoke(this, EventArgs.Empty);
}
private string selectedAddress;
public string SelectedAddress {
get => selectedAddress;
private set => TryUpdate(ref selectedAddress, value);
}
private void UpdateSelectedAddress() {
var dataIndex1 = scroll.ViewPointToDataIndex(SelectionStart);
var dataIndex2 = scroll.ViewPointToDataIndex(SelectionEnd);
var left = Math.Min(dataIndex1, dataIndex2);
var result = "Address: " + left.ToString("X6");
var elementName = BuildElementName(Model, left);
if (!string.IsNullOrWhiteSpace(elementName)) result += $" | {elementName}";
if (!SelectionStart.Equals(SelectionEnd)) {
int length = Math.Abs(dataIndex1 - dataIndex2) + 1;
result += $" | {length} bytes selected";
}
SelectedAddress = result;
}
public static string BuildElementName(IDataModel model, int address) {
var run = model.GetNextRun(address);
if (run is ITableRun array1 && array1.Start <= address) {
var index = array1.ConvertByteOffsetToArrayOffset(address).ElementIndex;
var basename = model.GetAnchorFromAddress(-1, array1.Start);
if (array1.ElementNames.Count > index) {
return $"{basename}/{array1.ElementNames[index]}";
} else {
return $"{basename}/{index}";
}
} else if (run.PointerSources != null && run.PointerSources.Count > 0 && string.IsNullOrEmpty(model.GetAnchorFromAddress(-1, run.Start))) {
var sourceRun = model.GetNextRun(run.PointerSources[0]);
if (sourceRun is ITableRun array2) {
// we are an anchor that's pointed to from an array
var offset = array2.ConvertByteOffsetToArrayOffset(run.PointerSources[0]);
var index = offset.ElementIndex;
if (index >= 0) {
var segment = array2.ElementContent[offset.SegmentIndex];
var basename = model.GetAnchorFromAddress(-1, array2.Start);
if (array2.ElementNames.Count > index) {
return $"{basename}/{array2.ElementNames[index]}/{segment.Name}";
} else {
return $"{basename}/{index}/{segment.Name}";
}
}
}
}
return string.Empty;
}
private string selectedBytes;
public string SelectedBytes {
get {
if (selectedBytes != null) return selectedBytes;
var bytes = GetSelectedByteContents(0x10);
selectedBytes = "Selected Bytes: " + bytes;
return selectedBytes;
}
private set => TryUpdate(ref selectedBytes, value);
}
// update the selected bytes lazily. Most of the time we don't really care about the new value.
private void UpdateSelectedBytes() => SelectedBytes = null;
private string GetSelectedByteContents(int maxByteCount = int.MaxValue) {
var dataIndex1 = scroll.ViewPointToDataIndex(SelectionStart);
var dataIndex2 = scroll.ViewPointToDataIndex(SelectionEnd);
var left = Math.Min(dataIndex1, dataIndex2);
var length = Math.Abs(dataIndex1 - dataIndex2) + 1;
if (left < 0) { length += left; left = 0; }
if (left + length > Model.Count) length = Model.Count - left;
var result = new StringBuilder();
for (int i = 0; i < length && i < maxByteCount; i++) {
var token = Model[left + i].ToHexString();
result.Append(token);
result.Append(" ");
}
if (maxByteCount < length) result.Append("...");
return result.ToString();
}
private void SelectAllExecuted() {
Goto.Execute(0);
SelectionStart = new Point(0, 0);
SelectionEnd = scroll.DataIndexToViewPoint(Model.Count - 1);
}
#endregion
#region Undo / Redo
private readonly ChangeHistory history;
public ChangeHistory ChangeHistory => history;
public ModelDelta CurrentChange => history.CurrentChange;
public ICommand Undo => undoWrapper;
public ICommand Redo => redoWrapper;
private ModelDelta RevertChanges(ModelDelta changes) {
var reverse = changes.Revert(Model);
RefreshBackingData();
return reverse;
}
private void HistoryPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(history.IsSaved)) {
save.RaiseCanExecuteChanged();
exportBackup.RaiseCanExecuteChanged();
if (history.IsSaved) { Model.ResetChanges(); RefreshBackingData(); }
}
if (e.PropertyName == nameof(history.HasDataChange)) NotifyPropertyChanged(nameof(Name));
}
#endregion
#region Saving
private readonly StubCommand
save = new StubCommand(),
saveAs = new StubCommand(),
exportBackup = new StubCommand(),
close = new StubCommand();
public ICommand Save => save;
public ICommand SaveAs => saveAs;
public ICommand ExportBackup => exportBackup;
public ICommand Close => close;
public event EventHandler Closed;
private void SaveExecuted(IFileSystem fileSystem) {
if (history.IsSaved) return;
if (string.IsNullOrEmpty(FileName)) {
SaveAsExecuted(fileSystem);
return;
}
var metadata = Model.ExportMetadata(Singletons.MetadataInfo);
if (fileSystem.Save(new LoadedFile(FileName, Model.RawData))) {
fileSystem.SaveMetadata(FileName, metadata?.Serialize());
history.TagAsSaved();
Model.ResetChanges();
}
}
private void SaveAsExecuted(IFileSystem fileSystem) {
var newName = fileSystem.RequestNewName(FileName, "GameBoy Advanced", "gba");
if (newName == null) return;
var metadata = Model.ExportMetadata(Singletons.MetadataInfo);
if (fileSystem.Save(new LoadedFile(newName, Model.RawData))) {
FileName = newName; // don't bother notifying, because tagging the history will cause a notify;
fileSystem.SaveMetadata(FileName, metadata?.Serialize());
history.TagAsSaved();
Model.ResetChanges();
}
}
private void ExportBackupExecuted(IFileSystem fileSystem) {
var changeDescription = fileSystem.RequestText("Export Summary", "What was your most recent change?");
if (changeDescription == null) return;
changeDescription = new string(changeDescription.Where(char.IsLetterOrDigit).ToArray());
var exportID = Model.NextExportID;
Model.NextExportID += 1;
var metadata = Model.ExportMetadata(Singletons.MetadataInfo);
var fileName = Path.GetFileNameWithoutExtension(FullFileName);
fileName = fileName.Split("_backup")[0];
var extension = Path.GetExtension(FullFileName);
var directory = Path.GetDirectoryName(FullFileName);
if (!string.IsNullOrEmpty(directory)) directory = directory + Path.DirectorySeparatorChar;
var exportName = $"{directory}backups{Path.DirectorySeparatorChar}{fileName}_backup{exportID}__{changeDescription}{extension}";
if (fileSystem.Save(new LoadedFile(exportName, Model.RawData))) {
fileSystem.SaveMetadata(exportName, metadata?.Serialize());
}
}
private void CloseExecuted(IFileSystem fileSystem) {
if (!history.IsSaved) {
var metadata = Model.ExportMetadata(Singletons.MetadataInfo);
var result = fileSystem.TrySavePrompt(new LoadedFile(FileName, Model.RawData));
if (result == null) return;
if (result == true) {
fileSystem.SaveMetadata(FileName, metadata?.Serialize());
}
}
Closed?.Invoke(this, EventArgs.Empty);
}
#endregion
#region Progress
private double progress;
public double Progress { get => progress; set => Set(ref progress, value); }
private bool updateInProgress;
public bool UpdateInProgress { get => updateInProgress; set => Set(ref updateInProgress, value); }
private int initialWorkLoad;
private readonly List CurrentProgressScopes = new List();
#endregion
public int FreeSpaceStart { get => Model.FreeSpaceStart; set {
if (Model.FreeSpaceStart != value) {
Model.FreeSpaceStart = value;
NotifyPropertyChanged();
}
}
}
private StubCommand gotoFreeSpaceStart;
public ICommand GotoFreeSpaceStart => StubCommand(ref gotoFreeSpaceStart, () => Goto.Execute(Model.FreeSpaceStart));
private readonly ToolTray tools;
public bool HasTools => true;
public IToolTrayViewModel Tools => tools;
private bool anchorTextVisible;
public bool AnchorTextVisible {
get => anchorTextVisible;
set => Set(ref anchorTextVisible, value);
}
private string anchorText;
public string AnchorText {
get => anchorText;
set {
if (value == null) value = string.Empty;
if (!value.StartsWith(AnchorStart.ToString())) value = AnchorStart + value;
if (TryUpdate(ref anchorText, value)) {
var index = scroll.ViewPointToDataIndex(SelectionStart);
var run = Model.GetNextRun(index);
if (run.Start <= index) {
var token = new NoDataChangeDeltaModel();
var errorInfo = PokemonModel.ApplyAnchor(Model, token, run.Start, AnchorText);
if (errorInfo == ErrorInfo.NoError) {
OnError?.Invoke(this, string.Empty);
var newRun = Model.GetNextRun(index);
if (AnchorText == AnchorStart.ToString()) Model.ClearFormat(token, run.Start, 1);
if (newRun is ArrayRun array) {
// if the format changed (ignoring length), run a goto to update the display width
if (run is ArrayRun array2 && !array.HasSameSegments(array2)) {
selection.PropertyChanged -= SelectionPropertyChanged; // to keep from double-updating the AnchorText
Goto.Execute(index);
selection.PropertyChanged += SelectionPropertyChanged;
}
UpdateColumnHeaders();
}
Tools.RefreshContent();
RefreshBackingData();
} else {
OnError?.Invoke(this, errorInfo.ErrorMessage);
}
if (token.HasAnyChange) history.InsertCustomChange(token);
}
}
}
}
private int anchorTextSelectionStart;
public int AnchorTextSelectionStart { get => anchorTextSelectionStart; set => Set(ref anchorTextSelectionStart, value); }
private int anchorTextSelectionLength;
public int AnchorTextSelectionLength { get => anchorTextSelectionLength; set => Set(ref anchorTextSelectionLength, value); }
public ICommand Copy => copy;
public ICommand CopyAddress => copyAddress;
public ICommand CopyBytes => copyBytes;
public ICommand DeepCopy => deepCopy;
public ICommand Clear => clear;
public ICommand IsText => isText;
public HexElement this[Point p] => this[p.X, p.Y];
public HexElement this[int x, int y] {
get {
if (x < 0 || x >= Width || x >= currentView.GetLength(0)) return HexElement.Undefined;
if (y < 0 || y >= Height || y >= currentView.GetLength(1)) return HexElement.Undefined;
if (currentView[x, y] is object) return currentView[x, y];
if (x == 0 && y == 0) {
RefreshBackingDataFull();
return currentView[x, y];
}
using (ModelCacheScope.CreateScope(Model)) {
RefreshBackingData(new Point(x, y));
}
return currentView[x, y];
}
}
public IDataModel Model { get; }
public bool FormattedDataIsSelected {
get {
var (left, right) = (scroll.ViewPointToDataIndex(SelectionStart), scroll.ViewPointToDataIndex(SelectionEnd));
if (left > right) (left, right) = (right, left);
var nextRun = Model.GetNextRun(left);
return nextRun.Start <= right;
}
}
#pragma warning disable 0067 // it's ok if events are never used
public event EventHandler OnError;
public event EventHandler OnMessage;
public event EventHandler ClearMessage;
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event EventHandler RequestTabChange;
public event EventHandler RequestDelayedWork;
public event EventHandler RequestMenuClose;
#pragma warning restore 0067
#region Constructors
public ViewPort() : this(new LoadedFile(string.Empty, new byte[0])) { }
public ViewPort(string fileName, IDataModel model, IWorkDispatcher dispatcher, Singletons singletons = null, ChangeHistory changeHistory = null) {
Singletons = singletons ?? new Singletons();
history = changeHistory ?? new ChangeHistory(RevertChanges);
history.PropertyChanged += HistoryPropertyChanged;
this.dispatcher = dispatcher ?? InstantDispatch.Instance;
Model = model;
FileName = fileName;
ColumnHeaders = new ObservableCollection();
scroll = new ScrollRegion(model.TryGetUsefulHeader) { DataLength = Model.Count };
scroll.PropertyChanged += ScrollPropertyChanged;
selection = new Selection(scroll, Model, history, GetSelectionSpan);
selection.PropertyChanged += SelectionPropertyChanged;
selection.PreviewSelectionStartChanged += ClearActiveEditBeforeSelectionChanges;
selection.OnError += (sender, e) => OnError?.Invoke(this, e);
tools = new ToolTray(Singletons, Model, selection, history, this);
Tools.OnError += (sender, e) => OnError?.Invoke(this, e);
Tools.OnMessage += (sender, e) => RaiseMessage(e);
tools.RequestMenuClose += (sender, e) => RequestMenuClose?.Invoke(this, e);
Tools.StringTool.ModelDataChanged += ModelChangedByTool;
Tools.StringTool.ModelDataMoved += ModelDataMovedByTool;
Tools.TableTool.ModelDataChanged += ModelChangedByTool;
Tools.TableTool.ModelDataMoved += ModelDataMovedByTool;
Tools.CodeTool.ModelDataChanged += ModelChangedByCodeTool;
Tools.CodeTool.ModelDataMoved += ModelDataMovedByTool;
scroll.Scheduler = tools;
ImplementCommands();
if (changeHistory == null) CascadeScripts(); // if we're sharing history with another viewmodel, our model has already been updated like this.
RefreshBackingData();
}
public ViewPort(LoadedFile file) : this(file.Name, new BasicModel(file.Contents), InstantDispatch.Instance) { }
private void ImplementCommands() {
undoWrapper.CanExecute = history.Undo.CanExecute;
undoWrapper.Execute = arg => { history.Undo.Execute(arg); using (ModelCacheScope.CreateScope(Model)) tools.RefreshContent(); };
history.Undo.CanExecuteChanged += (sender, e) => undoWrapper.CanExecuteChanged.Invoke(undoWrapper, e);
redoWrapper.CanExecute = history.Redo.CanExecute;
redoWrapper.Execute = arg => { history.Redo.Execute(arg); using (ModelCacheScope.CreateScope(Model)) tools.RefreshContent(); };
history.Redo.CanExecuteChanged += (sender, e) => redoWrapper.CanExecuteChanged.Invoke(redoWrapper, e);
clear.CanExecute = CanAlwaysExecute;
clear.Execute = arg => {
var selectionStart = scroll.ViewPointToDataIndex(selection.SelectionStart);
var selectionEnd = scroll.ViewPointToDataIndex(selection.SelectionEnd);
var left = Math.Min(selectionStart, selectionEnd);
var right = Math.Max(selectionStart, selectionEnd);
var startRun = Model.GetNextRun(left);
var endRun = Model.GetNextRun(right);
if (startRun == endRun && startRun.Start <= left && (startRun.Start < left || startRun.Start + startRun.Length - 1 > right) && startRun is ITableRun arrayRun) {
for (int i = 0; i < arrayRun.ElementCount; i++) {
var start = arrayRun.Start + arrayRun.ElementLength * i;
if (start + arrayRun.ElementLength <= left) continue;
if (start > right) break;
for (int j = 0; j < arrayRun.ElementLength; j++) history.CurrentChange.ChangeData(Model, start + j, 0xFF);
}
} else if (startRun == endRun && (startRun.Start right + 1)) {
// clearing _within_ a single run
Model.ClearData(history.CurrentChange, left, right - left + 1);
} else {
Model.ClearFormatAndData(history.CurrentChange, left, right - left + 1);
}
RefreshBackingData();
};
copy.CanExecute = CanAlwaysExecute;
copy.Execute = arg => {
var selectionStart = scroll.ViewPointToDataIndex(selection.SelectionStart);
var selectionEnd = scroll.ViewPointToDataIndex(selection.SelectionEnd);
var left = Math.Min(selectionStart, selectionEnd);
var length = Math.Abs(selectionEnd - selectionStart) + 1;
if (length > CopyLimit) {
OnError?.Invoke(this, $"Cannot copy more than {CopyLimit} bytes at once!");
} else {
bool usedHistory = false;
if (left + length > Model.Count) {
OnError?.Invoke(this, $"Cannot copy beyond the end of the data.");
} else if (left < 0) {
OnError?.Invoke(this, $"Cannot copy before the start of the data.");
} else {
((IFileSystem)arg).CopyText = Model.Copy(() => { usedHistory = true; return history.CurrentChange; }, left, length);
RefreshBackingData();
if (usedHistory) UpdateToolsFromSelection(left);
}
}
RequestMenuClose?.Invoke(this, EventArgs.Empty);
};
copyAddress.CanExecute = CanAlwaysExecute;
copyAddress.Execute = arg => {
var fileSystem = (IFileSystem)arg;
CopyAddressExecute(fileSystem);
};
copyBytes.CanExecute = CanAlwaysExecute;
copyBytes.Execute = arg => {
var fileSystem = (IFileSystem)arg;
CopyBytesExecute(fileSystem);
};
deepCopy.CanExecute = CanAlwaysExecute;
deepCopy.Execute = arg => {
var fileSystem = (IFileSystem)arg;
DeepCopyExecute(fileSystem);
};
moveSelectionStart.CanExecute = selection.MoveSelectionStart.CanExecute;
moveSelectionStart.Execute = arg => {
var direction = (Direction)arg;
using (ModelCacheScope.CreateScope(Model)) {
MoveSelectionStartExecuted(arg, direction);
}
};
selection.MoveSelectionStart.CanExecuteChanged += (sender, e) => moveSelectionStart.CanExecuteChanged.Invoke(this, e);
moveSelectionEnd.CanExecute = selection.MoveSelectionEnd.CanExecute;
moveSelectionEnd.Execute = arg => {
using (ModelCacheScope.CreateScope(Model)) {
selection.MoveSelectionEnd.Execute(arg);
}
};
selection.MoveSelectionEnd.CanExecuteChanged += (sender, e) => moveSelectionEnd.CanExecuteChanged.Invoke(this, e);
isText.CanExecute = CanAlwaysExecute;
isText.Execute = IsTextExecuted;
save.CanExecute = arg => !history.IsSaved;
save.Execute = arg => SaveExecuted((IFileSystem)arg);
saveAs.CanExecute = CanAlwaysExecute;
saveAs.Execute = arg => SaveAsExecuted((IFileSystem)arg);
exportBackup.CanExecute = arg => !history.HasDataChange;
exportBackup.Execute = arg => ExportBackupExecuted((IFileSystem)arg);
close.CanExecute = CanAlwaysExecute;
close.Execute = arg => CloseExecuted((IFileSystem)arg);
selectAll.CanExecute = CanAlwaysExecute;
selectAll.Execute = arg => SelectAllExecuted();
}
///
/// Top-level scripts may be available through metadata.
/// Find scripts called by those scripts, and add runs for those too.
///
private void CascadeScripts() {
var noChange = new NoDataChangeDeltaModel();
using (ModelCacheScope.CreateScope(Model)) {
foreach (var run in Runs(Model).OfType().ToList()) {
if (run is XSERun) {
tools.CodeTool.ScriptParser.FormatScript(noChange, Model, run.Start);
} else if (run is BSERun) {
tools.CodeTool.BattleScriptParser.FormatScript(noChange, Model, run.Start);
} else if (run is ASERun) {
tools.CodeTool.AnimationScriptParser.FormatScript(noChange, Model, run.Start);
}
}
}
}
private static IEnumerable Runs(IDataModel model) {
for (var run = model.GetNextRun(0); run.Start < model.Count; run = model.GetNextRun(run.Start + Math.Max(1, run.Length))) {
yield return run;
}
}
private void CopyAddressExecute(IFileSystem fileSystem) {
var copyText = scroll.ViewPointToDataIndex(selection.SelectionStart).ToString("X6");
fileSystem.CopyText = copyText;
RequestMenuClose?.Invoke(this, EventArgs.Empty);
OnMessage?.Invoke(this, $"'{copyText}' copied to clipboard.");
}
private void CopyBytesExecute(IFileSystem fileSystem) {
var copyText = GetSelectedByteContents();
fileSystem.CopyText = copyText;
RequestMenuClose?.Invoke(this, EventArgs.Empty);
OnMessage?.Invoke(this, $"'{copyText}' copied to clipboard.");
}
private void DeepCopyExecute(IFileSystem fileSystem) {
var selectionStart = scroll.ViewPointToDataIndex(selection.SelectionStart);
var selectionEnd = scroll.ViewPointToDataIndex(selection.SelectionEnd);
var left = Math.Min(selectionStart, selectionEnd);
var length = Math.Abs(selectionEnd - selectionStart) + 1;
if (length > CopyLimit) {
OnError?.Invoke(this, $"Cannot copy more than {CopyLimit} bytes at once!");
} else {
bool usedHistory = false;
fileSystem.CopyText = Model.Copy(() => { usedHistory = true; return history.CurrentChange; }, left, length, deep: true);
RefreshBackingData();
if (usedHistory) UpdateToolsFromSelection(left);
}
RequestMenuClose?.Invoke(this, EventArgs.Empty);
}
#endregion
private void MoveSelectionStartExecuted(object arg, Direction direction) {
var format = this[SelectionStart.X, SelectionStart.Y].Format;
if (format is UnderEdit underEdit && underEdit.AutocompleteOptions != null && underEdit.AutocompleteOptions.Count > 0) {
int index = -1;
for (int i = 0; i < underEdit.AutocompleteOptions.Count; i++) if (underEdit.AutocompleteOptions[i].IsSelected) index = i;
var options = default(IReadOnlyList);
if (direction == Direction.Up) {
index -= 1;
if (index < -1) index = underEdit.AutocompleteOptions.Count - 1;
options = AutoCompleteSelectionItem.Generate(underEdit.AutocompleteOptions.Select(option => option.CompletionText), index);
} else if (direction == Direction.Down) {
index += 1;
if (index == underEdit.AutocompleteOptions.Count) index = -1;
options = AutoCompleteSelectionItem.Generate(underEdit.AutocompleteOptions.Select(option => option.CompletionText), index);
}
if (options != null) {
var edit = new UnderEdit(underEdit.OriginalFormat, underEdit.CurrentText, underEdit.EditWidth, options);
currentView[SelectionStart.X, SelectionStart.Y] = new HexElement(this[SelectionStart.X, SelectionStart.Y], edit);
NotifyCollectionChanged(ResetArgs);
return;
}
}
PreviewScrollChanged?.Invoke(this, EventArgs.Empty);
selection.MoveSelectionStart.Execute(arg);
}
public Point ConvertAddressToViewPoint(int address) => scroll.DataIndexToViewPoint(address);
public int ConvertViewPointToAddress(Point p) => scroll.ViewPointToDataIndex(p);
public IReadOnlyList GetContextMenuItems(Point selectionPoint) {
Debug.Assert(IsSelected(selectionPoint));
var factory = new ContextItemFactory(this);
var cell = this[SelectionStart.X, SelectionStart.Y];
(cell?.Format ?? None.Instance).Visit(factory, cell.Value);
var results = factory.Results.ToList();
if (!SelectionStart.Equals(SelectionEnd)) {
results.Add(new ContextItem("Copy", Copy.Execute) { ShortcutText = "Ctrl+C" });
results.Add(new ContextItem("Deep Copy", DeepCopy.Execute) { ShortcutText = "Ctrl+Shift+C" });
}
results.Add(new ContextItem("Paste", arg => Edit(((IFileSystem)arg).CopyText)) { ShortcutText = "Ctrl+V" });
results.Add(new ContextItem("Copy Address", arg => CopyAddressExecute((IFileSystem)arg)));
return results;
}
public bool IsSelected(Point point) => selection.IsSelected(point);
public bool IsTable(Point point) {
var search = scroll.ViewPointToDataIndex(point);
var run = Model.GetNextRun(search);
return run.Start <= search && run is ITableRun;
}
public void Refresh() {
scroll.DataLength = Model.Count;
RefreshBackingData();
Tools.TableTool.DataForCurrentRunChanged();
Tools.SpriteTool.DataForCurrentRunChanged();
}
public bool TryImport(LoadedFile file, IFileSystem fileSystem) {
if (file.Name.ToLower().EndsWith(".hma")) {
var edit = Encoding.Default.GetString(file.Contents);
Edit(edit);
return true;
}
return false;
}
public void RaiseError(string text) => OnError?.Invoke(this, text);
private string deferredMessage;
public void RaiseMessage(string text) {
// TODO queue multiple messages.
deferredMessage = text;
tools.Schedule(RaiseMessage);
}
private void RaiseMessage() => OnMessage?.Invoke(this, deferredMessage);
public void ClearAnchor() {
var startDataIndex = scroll.ViewPointToDataIndex(SelectionStart);
var endDataIndex = scroll.ViewPointToDataIndex(SelectionEnd);
if (startDataIndex > endDataIndex) (startDataIndex, endDataIndex) = (endDataIndex, startDataIndex);
// do the clear with a custom token that can't change data.
// This anchor-clear is a formatting-only change.
Model.ClearAnchor(history.InsertCustomChange(new NoDataChangeDeltaModel()), startDataIndex, endDataIndex - startDataIndex + 1);
Refresh();
}
private readonly IWorkDispatcher dispatcher;
public void Edit(string input) {
if (!UpdateInProgress) {
UpdateInProgress = true;
CurrentProgressScopes.Insert(0, tools.DeferUpdates);
CurrentProgressScopes.Insert(0, ModelCacheScope.CreateScope(Model));
initialWorkLoad = input.Length;
}
// allow chunking at newline boundaries only
int chunkSize = Math.Max(200, initialWorkLoad / 100);
var maxSize = input.Length;
if (dispatcher != null && input.Length > chunkSize) {
var nextNewline = input.Substring(chunkSize).IndexOf('\n');
if (nextNewline != -1) maxSize = chunkSize + nextNewline + 1;
}
exitEditEarly = false;
int i = 0;
try {
for (i = 0; i < input.Length && i < maxSize && !exitEditEarly; i++) {
if (input[i] == '@' && input.Substring(i).StartsWith("@!game")) skipToNextGameCode = false;
if (skipToNextGameCode) {
// skip this input
} else if (input[i] == '.' && input.Length > i + 6 && input.Substring(i + 1, 5).ToLower() == "thumb") {
var lines = input.Substring(i).Split('\n', '\r');
var endLine = lines.Length.Range().FirstOrDefault(j => (lines[j] + " ").ToLower().StartsWith(".end "));
if (endLine == 0) endLine = lines.Length - 1;
lines = lines.Take(endLine + 1).ToArray();
var thumbLength = (lines.Length - 1) + lines.Sum(line => line.Length);
i += thumbLength - 1;
InsertThumbCode(lines);
} else {
Edit(input[i]);
}
}
} catch {
ClearEditWork();
throw;
}
if (exitEditEarly) {
ClearEditWork();
Refresh();
} else if (input.Length > i) {
Progress = (double)(initialWorkLoad - input.Length) / initialWorkLoad;
dispatcher.DispatchWork(() => Edit(input.Substring(i)));
} else {
ClearEditWork();
}
}
public void InsertThumbCode(string[] lines) {
var start = ConvertViewPointToAddress(SelectionStart);
var result = tools.CodeTool.Parser.Compile(Model, start, lines);
for (int i = 0; i < result.Count; i++) CurrentChange.ChangeData(Model, start + i, result[i]);
SelectionStart = ConvertAddressToViewPoint(start + result.Count);
RefreshBackingData();
}
private void ClearEditWork() {
CurrentProgressScopes.ForEach(scope => scope.Dispose());
CurrentProgressScopes.Clear();
UpdateInProgress = false;
skipToNextGameCode = false;
}
public void Edit(ConsoleKey key) {
using (ModelCacheScope.CreateScope(Model)) {
var offset = scroll.ViewPointToDataIndex(GetEditPoint());
var run = Model.GetNextRun(offset);
var point = GetEditPoint();
var element = this[point.X, point.Y];
var underEdit = element.Format as UnderEdit;
if (key == ConsoleKey.Enter && underEdit != null) {
if (underEdit.AutocompleteOptions != null && underEdit.AutocompleteOptions.Any(option => option.IsSelected)) {
var selectedIndex = AutoCompleteSelectionItem.SelectedIndex(underEdit.AutocompleteOptions);
underEdit = new UnderEdit(underEdit.OriginalFormat, underEdit.AutocompleteOptions[selectedIndex].CompletionText, underEdit.EditWidth);
currentView[point.X, point.Y] = new HexElement(element.Value, element.Edited, underEdit);
RequestMenuClose?.Invoke(this, EventArgs.Empty);
TryCompleteEdit(point);
} else {
Edit(Environment.NewLine);
}
return;
}
if (key == ConsoleKey.Enter && run is ITableRun arrayRun1) {
var offsets = arrayRun1.ConvertByteOffsetToArrayOffset(offset);
SilentScroll(offsets.SegmentStart + arrayRun1.ElementLength);
}
if (key == ConsoleKey.Tab && run is ITableRun arrayRun2) {
var offsets = arrayRun2.ConvertByteOffsetToArrayOffset(offset);
SilentScroll(offsets.SegmentStart + arrayRun2.ElementContent[offsets.SegmentIndex].Length);
}
if (key == ConsoleKey.Escape) {
ClearEdits(SelectionStart);
ClearMessage?.Invoke(this, EventArgs.Empty);
RequestMenuClose?.Invoke(this, EventArgs.Empty);
}
if (key != ConsoleKey.Backspace) return;
AcceptBackspace(underEdit, element.Value, point);
}
}
public void Autocomplete(string input) {
var point = SelectionStart;
var element = this[point.X, point.Y];
var underEdit = element.Format as UnderEdit;
if (underEdit == null) return;
bool tryComplete = true;
if (underEdit.AutocompleteOptions != null) {
var options = underEdit.AutocompleteOptions;
var index = options.Select(option => option.CompletionText).ToList().IndexOf(input);
underEdit = new UnderEdit(underEdit.OriginalFormat, options[index].CompletionText, underEdit.EditWidth);
tryComplete = options[index].IsFormatComplete;
} else {
underEdit = new UnderEdit(underEdit.OriginalFormat, input, underEdit.EditWidth);
}
currentView[point.X, point.Y] = new HexElement(element.Value, element.Edited, underEdit);
if (tryComplete) {
TryCompleteEdit(point);
} else {
NotifyCollectionChanged(ResetArgs); // refresh the view
}
}
public void RepointToNewCopy(int pointer) {
// if the pointer points to nothing
var destinationAddress = Model.ReadPointer(pointer);
if (destinationAddress == Pointer.NULL) {
CreateNewData(pointer);
return;
}
// if the pointer is expected to point to a type of data, but doesn't
var destination = Model.GetNextRun(destinationAddress);
var parentRun = Model.GetNextRun(pointer);
if (parentRun is ITableRun tableRun) {
var offset = tableRun.ConvertByteOffsetToArrayOffset(pointer);
if (tableRun.ElementContent[offset.SegmentIndex] is ArrayRunPointerSegment pSegment) {
var run = destination;
var error = FormatRunFactory.GetStrategy(pSegment.InnerFormat).TryParseData(Model, string.Empty, destinationAddress, ref run);
if (error.HasError) {
CreateNewData(pointer);
return;
}
}
}
// if the pointer points to the right type of data, but with no run
if (destination.Start != destinationAddress) {
RepointWithoutRun(pointer, destinationAddress);
return;
}
if (destination.PointerSources.Count < 2) {
OnError?.Invoke(this, "This is the only pointer, no need to make a new copy.");
return;
}
if (destination is ArrayRun) {
OnError?.Invoke(this, "Cannot automatically duplicate a table. This operation is unsafe.");
return;
}
var newDestination = Model.FindFreeSpace(destination.Start, destination.Length);
if (newDestination == -1) {
newDestination = Model.Count;
Model.ExpandData(history.CurrentChange, Model.Count + destination.Length);
}
for (int i = 0; i < destination.Length; i++) {
history.CurrentChange.ChangeData(Model, newDestination + i, Model[destination.Start + i]);
}
Model.ClearPointer(CurrentChange, pointer, destination.Start);
Model.WritePointer(CurrentChange, pointer, newDestination); // point to the new destination
var destination2 = Model.GetNextRun(destination.Start);
Model.ObserveRunWritten(CurrentChange, destination2.Duplicate(newDestination, new SortedSpan(pointer))); // create a new run at the new destination
OnMessage?.Invoke(this, "New Copy added at " + newDestination.ToString("X6"));
Refresh();
}
public void OpenInNewTab(int destination) {
var child = new ViewPort(FileName, Model, dispatcher, Singletons, history);
child.selection.GotoAddress(destination);
RequestTabChange?.Invoke(this, child);
}
private bool CreateNewData(int pointer) {
var errorText = "Can only create new data for a pointer with a format within a table.";
if (!(Model.GetNextRun(pointer) is ITableRun tableRun)) {
OnError?.Invoke(this, errorText);
return false;
}
var offsets = tableRun.ConvertByteOffsetToArrayOffset(pointer);
if (!(tableRun.ElementContent[offsets.SegmentIndex] is ArrayRunPointerSegment pointerSegment) || !pointerSegment.IsInnerFormatValid) {
OnError?.Invoke(this, errorText);
return false;
}
var length = FormatRunFactory.GetStrategy(pointerSegment.InnerFormat).LengthForNewRun(Model, pointer);
var insert = Model.FindFreeSpace(0, length);
if (insert < 0) {
insert = Model.Count;
Model.ExpandData(CurrentChange, Model.Count + length);
scroll.DataLength = Model.Count;
}
pointerSegment.WriteNewFormat(Model, CurrentChange, pointer, insert, tableRun.ElementContent);
RaiseMessage($"New data added at {insert:X6}");
RefreshBackingData();
return true;
}
///
/// Sometimes, valid data exists in the game but no run could be added.
/// In such cases, it could be because a conflict was detected between 2 runs in the data.
/// Make a new copy of data, only if we can prove that the data is valid and only if no run is found.
/// Leave the original data (and other pointers to it) untouched.
///
private void RepointWithoutRun(int source, int destination) {
if (!(Model.GetNextRun(source) is ITableRun table)) {
RaiseError("Could not parse a data format for that pointer.");
return;
}
var offset = table.ConvertByteOffsetToArrayOffset(source);
var segment = table.ElementContent[offset.SegmentIndex] as ArrayRunPointerSegment;
if (segment == null) {
RaiseError("Could not parse a data format for that pointer.");
return;
}
var strategy = FormatRunFactory.GetStrategy(segment.InnerFormat);
if (strategy == null) {
RaiseError("Could not parse a data format for that pointer.");
return;
}
IFormattedRun run = new NoInfoRun(destination, new SortedSpan(source));
if (strategy.TryParseData(Model, string.Empty, destination, ref run).HasError) {
RaiseError("Could not parse a data format for that pointer.");
return;
}
var newDestination = Model.FindFreeSpace(destination, run.Length);
if (newDestination == -1) {
newDestination = Model.Count;
Model.ExpandData(history.CurrentChange, Model.Count + run.Length);
}
for (int i = 0; i < run.Length; i++) {
history.CurrentChange.ChangeData(Model, newDestination + i, Model[destination + i]);
}
Model.WritePointer(CurrentChange, source, newDestination); // point to the new destination
var newRun = run.Duplicate(newDestination, new SortedSpan(source));
Model.ObserveRunWritten(CurrentChange, newRun); // create a new run at the new destination
OnMessage?.Invoke(this, $"Run moved to {newDestination:X6}. This pointer was updated, original data was not modified.");
Refresh();
}
private void AcceptBackspace(UnderEdit underEdit, byte cellValue, Point point) {
// backspace in progress with characters left: just clear a character
if (underEdit != null && underEdit.CurrentText.Length > 0) {
var newText = underEdit.CurrentText.Substring(0, underEdit.CurrentText.Length - 1);
var options = underEdit.AutocompleteOptions;
if (options != null) {
var selectedIndex = AutoCompleteSelectionItem.SelectedIndex(underEdit.AutocompleteOptions);
options = GetAutocompleteOptions(underEdit.OriginalFormat, cellValue, newText, selectedIndex);
}
var newFormat = new UnderEdit(underEdit.OriginalFormat, newText, underEdit.EditWidth, options);
currentView[point.X, point.Y] = new HexElement(this[point.X, point.Y], newFormat);
NotifyCollectionChanged(ResetArgs);
return;
}
var index = scroll.ViewPointToDataIndex(point);
// backspace on an empty element: clear the data from those cells
if (underEdit != null) {
var operation = new DataClear(Model, history.CurrentChange, index);
var currentValue = index < Model.Count ? Model[index] : (byte)0;
underEdit.OriginalFormat.Visit(operation, currentValue);
RefreshBackingData();
SelectionStart = scroll.DataIndexToViewPoint(index - 1);
point = GetEditPoint();
index = scroll.ViewPointToDataIndex(point);
}
var run = Model.GetNextRun(index);
if (run.Start > index) {
// no run: doing a raw edit.
SelectionStart = scroll.DataIndexToViewPoint(index);
var element = this[SelectionStart.X, SelectionStart.Y];
var text = element.Value.ToString("X2");
currentView[SelectionStart.X, SelectionStart.Y] = new HexElement(element, element.Format.Edit(text.Substring(0, text.Length - 1)));
NotifyCollectionChanged(ResetArgs);
return;
}
if (run is PCSRun || run is AsciiRun) {
for (int i = index; i < run.Start + run.Length; i++) history.CurrentChange.ChangeData(Model, i, 0xFF);
var length = PCSString.ReadString(Model, run.Start, true);
if (run is PCSRun) Model.ObserveRunWritten(history.CurrentChange, new PCSRun(Model, run.Start, length, run.PointerSources));
RefreshBackingData();
SelectionStart = scroll.DataIndexToViewPoint(index - 1);
return;
}
var cellToText = new ConvertCellToText(Model, run.Start);
var cell = this[point];
var format = cell.Format;
if (format is Anchor anchor) format = anchor.OriginalFormat;
void TableBackspace(int length) {
PrepareForMultiSpaceEdit(point, length);
cell.Format.Visit(cellToText, cell.Value);
var text = cellToText.Result;
if (format is BitArray) {
for (int i = 1; i < length; i++) {
var extraData = Model[scroll.ViewPointToDataIndex(point) + i];
cell.Format.Visit(cellToText, extraData);
text += cellToText.Result;
}
}
text = text.Substring(0, text.Length - 1);
currentView[point.X, point.Y] = new HexElement(cell, new UnderEdit(cell.Format, text, length));
}
if (run is ITableRun array) {
var offsets = array.ConvertByteOffsetToArrayOffset(index);
if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.PCS) {
for (int i = index + 1; i < offsets.SegmentStart + array.ElementContent[offsets.SegmentIndex].Length; i++) history.CurrentChange.ChangeData(Model, i, 0x00);
history.CurrentChange.ChangeData(Model, index, 0xFF);
RefreshBackingData();
SelectionStart = scroll.DataIndexToViewPoint(index - 1);
} else if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.Pointer) {
TableBackspace(4);
} else if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.Integer) {
TableBackspace(((Integer)format).Length);
} else if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.BitArray) {
TableBackspace(((BitArray)format).Length);
} else {
throw new NotImplementedException();
}
NotifyCollectionChanged(ResetArgs);
return;
}
if (run is EggMoveRun || run is PLMRun) {
PrepareForMultiSpaceEdit(point, 2);
cell.Format.Visit(cellToText, cell.Value);
var text = cellToText.Result;
text = text.Substring(0, text.Length - 1);
currentView[point.X, point.Y] = new HexElement(cell, new UnderEdit(cell.Format, text, 2));
NotifyCollectionChanged(ResetArgs);
return;
}
if (run.Start <= index && run.Start + run.Length > index) {
// I want to do a backspace at the end of this run
SelectionStart = scroll.DataIndexToViewPoint(run.Start);
var element = this[SelectionStart.X, SelectionStart.Y];
element.Format.Visit(cellToText, element.Value);
var text = cellToText.Result;
var editLength = 1;
if (element.Format is Pointer) editLength = 4;
for (int i = 0; i < run.Length; i++) {
var p = scroll.DataIndexToViewPoint(run.Start + i);
string editString = i == 0 ? text.Substring(0, text.Length - 1) : string.Empty;
if (i > 0) editLength = 1;
var newFormat = new UnderEdit(this[p.X, p.Y].Format, editString, editLength);
currentView[p.X, p.Y] = new HexElement(this[p.X, p.Y], newFormat);
}
}
NotifyCollectionChanged(ResetArgs);
}
#region Find
public IReadOnlyList<(int start, int end)> Find(string rawSearch) {
var results = new List<(int start, int end)>();
var cleanedSearchString = rawSearch.ToUpper();
var searchBytes = new List();
// it might be a string with no quotes, we should check for matches for that.
if (cleanedSearchString.Length > 3 && !cleanedSearchString.Contains(StringDelimeter) && !cleanedSearchString.All(AllHexCharacters.Contains)) {
results.AddRange(FindUnquotedText(cleanedSearchString, searchBytes));
}
// it might be a matched-word
var matchedWords = Model.GetMatchedWords(rawSearch);
if (matchedWords.Count > 0) {
results.AddRange(matchedWords.Select(word => (word, word)));
}
// it might be a pointer without angle braces
if (cleanedSearchString.Length == 6 && cleanedSearchString.All(AllHexCharacters.Contains)) {
searchBytes.AddRange(Parse(cleanedSearchString).Reverse().Append((byte)0x08).Select(b => (SearchByte)b));
results.AddRange(Model.Search(searchBytes).Select(result => (result, result + 3)));
}
// it might be a bl command
if (cleanedSearchString.StartsWith("BL ") && cleanedSearchString.Contains("<") && cleanedSearchString.EndsWith(">")) {
results.AddRange(FindBranchLink(cleanedSearchString));
}
// attempt to parse the search string fully
if (TryParseSearchString(searchBytes, cleanedSearchString, errorOnParseError: results.Count == 0)) {
// find matches
var textResults = Model.Search(searchBytes).Select(result => (result, result + searchBytes.Count - 1)).ToList();
results.AddRange(textResults);
// find data matches for the results that are in tables
foreach (var result in textResults) {
if (Model.GetNextRun(result.result) is ArrayRun parentArray && parentArray.LengthFromAnchor == string.Empty) {
results.AddRange(FindMatchingDataResultsFromArrayElement(parentArray, result.result));
}
}
}
// reorder the list to start at the current cursor position
results.Sort((a, b) => a.start.CompareTo(b.start));
var offset = scroll.ViewPointToDataIndex(SelectionStart);
var left = results.Where(result => result.start < offset);
var right = results.Where(result => result.start >= offset);
results = right.Concat(left).ToList();
NotifyNumberOfResults(rawSearch, results.Count);
return results;
}
private IEnumerable<(int start, int end)> FindBranchLink(string command) {
var addressStart = command.IndexOf(" <") + 2;
var addressEnd = command.LastIndexOf(">");
if (addressEnd < addressStart) yield break;
var addressText = command.Substring(addressStart, addressEnd - addressStart);
if (!int.TryParse(addressText, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out int address)) {
address = Model.GetAddressFromAnchor(CurrentChange, -1, addressText);
if (address < 0 || address >= Model.Count) yield break;
}
// I want to know, for any given point in the raw data, if it's possible a branch-link command pointing to `address`
// branch link commands are always 4 bytes and have the following format:
// 11111 #11 11110 #11, where #=pc+#*2+4
// note that this command is 4 bytes long, stored byte reversed. So in the data, it's:
// 8 bits: bits 11-18 of a 22 bit signed offset
// 8 bits:
// the low 3 bits are bits 19-21 of a 22 bit signed offset
// the high 5 bits are always 11110
// 8 bits: bits 0-7 of a 22 bit signed offset
// 8 bits:
// the low 3 bits are bits 8-10 of a 22 bit signed offset
// the high 5 bits are always 11111
// the command is always 2-byte aligned
//
// bit order is really weird (11-18, 19-21, 0-7, 8-10) because BL is made of **2** instructions,
// and each instruction is stored little-endian
// start as early as possible in the file: maximum offset, or offset for source=0
int offset = Math.Min(0b0111111111111111111111, (address - 4) / 2);
for (; true; offset--) { // traveling down the offsets means traveling up the source options
int source = address - 4 - offset * 2;
if (source + 4 > Model.RawData.Length) break;
if (Model.RawData[source + 2] != (byte)offset) continue; // check source+2 first because it's the simplest, and thus fastest
if (Model.RawData[source + 0] != (byte)(offset >> 11)) continue;
if (Model.RawData[source + 3] != (0b11111000 | (0b111 & offset >> 8))) continue;
if (Model.RawData[source + 1] != (0b11110000 | (0b111 & offset >> 19))) continue;
yield return (source, source + 3);
}
}
private IEnumerable<(int start, int end)> FindUnquotedText(string cleanedSearchString, List searchBytes) {
var pcsBytes = PCSString.Convert(cleanedSearchString);
pcsBytes.RemoveAt(pcsBytes.Count - 1); // remove the 0xFF that was added, since we're searching for a string segment instead of a whole string.
// only search for the string if every character in the search string is allowed
if (pcsBytes.Count != cleanedSearchString.Length) yield break;
searchBytes.AddRange(pcsBytes.Select(b => new PCSSearchByte(b)));
var textResults = Model.Search(searchBytes).ToList();
Model.ConsiderResultsAsTextRuns(history.CurrentChange, textResults);
foreach (var result in textResults) {
if (Model.GetNextRun(result) is ArrayRun parentArray && parentArray.LengthFromAnchor == string.Empty) {
foreach (var dataResult in FindMatchingDataResultsFromArrayElement(parentArray, result)) yield return dataResult;
}
yield return (result, result + pcsBytes.Count - 1);
}
// it could also be a list token. Look for matches among list enums.
foreach (var dataResult in Model.FindListUsages(cleanedSearchString)) yield return dataResult;
}
///
/// When performing a search, sometimes one of the search results is text from a table.
/// If so, then we also care about places where that table value is used.
/// This function finds uses of an element in a table.
///
private IEnumerable<(int start, int end)> FindMatchingDataResultsFromArrayElement(ArrayRun parentArray, int parentIndex) {
var offsets = parentArray.ConvertByteOffsetToArrayOffset(parentIndex);
var parentArrayName = Model.GetAnchorFromAddress(-1, parentArray.Start);
if (offsets.SegmentIndex == 0 && parentArray.ElementContent[offsets.SegmentIndex].Type == ElementContentType.PCS) {
var arrayUses = FindTableUsages(offsets, parentArrayName);
var streamUses = FindStreamUsages(offsets, parentArrayName);
return arrayUses.Concat(streamUses);
}
return Enumerable.Empty<(int, int)>();
}
private IEnumerable<(int start, int end)> FindTableUsages(ArrayOffset offsets, string parentArrayName) {
foreach (var child in Model.All()) {
// option 1: another table has a row named after this element
if (child is ArrayRun arrayRun && arrayRun.LengthFromAnchor == parentArrayName) {
var address = child.Start + child.ElementLength * offsets.ElementIndex;
yield return (address, address + child.ElementLength - 1);
}
// option 2: another table has an enum named after this element
var segmentOffset = 0;
foreach (var segment in child.ElementContent) {
if (!(segment is ArrayRunEnumSegment enumSegment) || enumSegment.EnumName != parentArrayName) {
segmentOffset += segment.Length;
continue;
}
for (int i = 0; i < child.ElementCount; i++) {
var address = child.Start + child.ElementLength * i + segmentOffset;
var enumValue = Model.ReadMultiByteValue(address, segment.Length);
if (enumValue != offsets.ElementIndex) continue;
yield return (address, address + segment.Length - 1);
}
segmentOffset += segment.Length;
}
}
}
private IEnumerable<(int start, int end)> FindStreamUsages(ArrayOffset offsets, string parentArrayName) {
foreach (var child in Model.Streams) {
// option 1: the value is used by egg moves
if (child is EggMoveRun eggRun) {
foreach (var result in eggRun.Search(parentArrayName, offsets.ElementIndex)) yield return result;
}
// option 2: the value is used by learnable moves
if (child is PLMRun plmRun && parentArrayName == HardcodeTablesModel.MoveNamesTable) {
foreach (var result in plmRun.Search(offsets.ElementIndex)) yield return result;
}
// option 3: the value is a move used by trainer teams
if (child is TrainerPokemonTeamRun team) {
foreach (var result in team.Search(parentArrayName, offsets.ElementIndex)) {
yield return (result, result + 1);
}
}
// option 3: the value is in an enum used by a custom table stream
if (child is TableStreamRun table) {
foreach (var result in table.Search(parentArrayName, offsets.ElementIndex)) yield return result;
}
}
}
private void NotifyNumberOfResults(string rawSearch, int results) {
if (results == 1) {
OnMessage?.Invoke(this, $"Found only 1 match for '{rawSearch}'.");
} else if (results > 1) {
OnMessage?.Invoke(this, $"Found {results} matches for '{rawSearch}'.");
}
}
private byte[] Parse(string content) {
var result = new byte[content.Length / 2];
for (int i = 0; i < result.Length; i++) {
var thisByte = content.Substring(i * 2, 2);
result[i] += (byte)(AllHexCharacters.IndexOf(thisByte[0]) * 0x10);
result[i] += (byte)AllHexCharacters.IndexOf(thisByte[1]);
}
return result;
}
private bool TryParseSearchString(List searchBytes, string cleanedSearchString, bool errorOnParseError) {
for (int i = 0; i < cleanedSearchString.Length;) {
if (cleanedSearchString[i] == ' ') {
i++;
continue;
}
if (cleanedSearchString[i] == PointerStart) {
if (!TryParsePointerSearchSegment(searchBytes, cleanedSearchString, ref i)) return false;
continue;
}
if (cleanedSearchString[i] == StringDelimeter) {
if (TryParseStringSearchSegment(searchBytes, cleanedSearchString, ref i)) continue;
}
if (cleanedSearchString.Length >= i + 2 && cleanedSearchString.Substring(i, 2) == "XX") {
searchBytes.Add(SearchByte.Wild);
i += 2;
continue;
}
if (cleanedSearchString.Length >= i + 2 && cleanedSearchString.Substring(i, 2).All(AllHexCharacters.Contains)) {
searchBytes.AddRange(Parse(cleanedSearchString.Substring(i, 2)).Select(b => (SearchByte)b));
i += 2;
continue;
}
if (errorOnParseError) OnError?.Invoke(this, $"Could not parse search term {cleanedSearchString.Substring(i)}");
return false;
}
return true;
}
private bool TryParsePointerSearchSegment(List searchBytes, string cleanedSearchString, ref int i) {
var pointerEnd = cleanedSearchString.IndexOf(PointerEnd, i);
if (pointerEnd == -1) { OnError(this, "Search mismatch: no closing >"); return false; }
var pointerContents = cleanedSearchString.Substring(i + 1, pointerEnd - i - 1);
var address = Model.GetAddressFromAnchor(history.CurrentChange, -1, pointerContents);
if (address != Pointer.NULL) {
searchBytes.Add((SearchByte)(address >> 0));
searchBytes.Add((SearchByte)(address >> 8));
searchBytes.Add((SearchByte)(address >> 16));
searchBytes.Add((SearchByte)0x08);
} else if (pointerContents.All(AllHexCharacters.Contains) && pointerContents.Length <= 6) {
searchBytes.AddRange(Parse(pointerContents).Reverse().Append((byte)0x08).Select(b => (SearchByte)b));
} else {
OnError(this, $"Could not parse pointer <{pointerContents}>");
return false;
}
i = pointerEnd + 1;
return true;
}
private bool TryParseStringSearchSegment(List searchBytes, string cleanedSearchString, ref int i) {
var endIndex = cleanedSearchString.IndexOf(StringDelimeter, i + 1);
while (endIndex > i && cleanedSearchString[endIndex - 1] == '\\') endIndex = cleanedSearchString.IndexOf(StringDelimeter, endIndex + 1);
if (endIndex > i) {
var pcsBytes = PCSString.Convert(cleanedSearchString.Substring(i, endIndex + 1 - i));
i = endIndex + 1;
if (i == cleanedSearchString.Length) pcsBytes.RemoveAt(pcsBytes.Count - 1);
searchBytes.AddRange(pcsBytes.Select(b => new PCSSearchByte(b)));
return true;
} else {
return false;
}
}
#endregion
public IChildViewPort CreateChildView(int startAddress, int endAddress) {
var child = new ChildViewPort(this, dispatcher, Singletons);
var run = Model.GetNextRun(startAddress);
if (run is ArrayRun array) {
var offsets = array.ConvertByteOffsetToArrayOffset(startAddress);
var lineStart = array.Start + array.ElementLength * offsets.ElementIndex;
child.Goto.Execute(lineStart.ToString("X2"));
child.SelectionStart = child.ConvertAddressToViewPoint(startAddress);
child.SelectionEnd = child.ConvertAddressToViewPoint(endAddress);
} else {
child.Goto.Execute(startAddress.ToString("X2"));
child.SelectionEnd = child.ConvertAddressToViewPoint(endAddress);
}
return child;
}
public void FollowLink(int x, int y) {
var format = this[x, y].Format;
if (format is Anchor anchor) format = anchor.OriginalFormat;
if (format is SpriteDecorator sprite) format = sprite.OriginalFormat;
using (ModelCacheScope.CreateScope(Model)) {
// follow pointer
if (format is Pointer pointer) {
if (pointer.Destination != Pointer.NULL) {
selection.GotoAddress(pointer.Destination);
} else if (string.IsNullOrEmpty(pointer.DestinationName)) {
OnError(this, $"null pointers point to nothing, so going to their source isn't possible.");
} else {
OnError(this, $"Pointer destination {pointer.DestinationName} not found.");
}
return;
}
// follow word value source
if (format is MatchedWord word) {
var address = Model.GetAddressFromAnchor(history.CurrentChange, -1, word.Name.Substring(2));
if (address == Pointer.NULL) {
OnError(this, $"No table with name '{word.Name.Substring(2)}' was found.");
} else {
selection.GotoAddress(address);
}
return;
}
// open tool
var byteOffset = scroll.ViewPointToDataIndex(new Point(x, y));
var currentRun = Model.GetNextRun(byteOffset);
if (currentRun is ISpriteRun) {
tools.SpriteTool.SpriteAddress = currentRun.Start;
tools.SelectedIndex = Tools.IndexOf(Tools.SpriteTool);
} else if (currentRun is IPaletteRun) {
tools.SpriteTool.PaletteAddress = currentRun.Start;
tools.SelectedIndex = Tools.IndexOf(Tools.SpriteTool);
} else if (currentRun is IStreamRun) {
Tools.StringTool.Address = currentRun.Start;
Tools.SelectedIndex = Tools.IndexOf(Tools.StringTool);
} else if (currentRun is ITableRun array) {
var offsets = array.ConvertByteOffsetToArrayOffset(byteOffset);
if (format is PCS) {
Tools.StringTool.Address = offsets.SegmentStart - offsets.ElementIndex * array.ElementLength;
Tools.SelectedIndex = Tools.IndexOf(Tools.StringTool);
} else {
Tools.TableTool.Address = array.Start + offsets.ElementIndex * array.ElementLength;
Tools.SelectedIndex = Tools.IndexOf(Tools.TableTool);
}
}
}
}
public void ExpandSelection(int x, int y) {
var index = scroll.ViewPointToDataIndex(SelectionStart);
var run = Model.GetNextRun(index);
if (run.Start > index) return;
if (run is ITableRun array) {
var offsets = array.ConvertByteOffsetToArrayOffset(index);
if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.Pointer) {
FollowLink(x, y);
} else {
SelectionStart = scroll.DataIndexToViewPoint(offsets.SegmentStart);
SelectionEnd = scroll.DataIndexToViewPoint(offsets.SegmentStart + array.ElementContent[offsets.SegmentIndex].Length - 1);
}
} else if (run is PointerRun) {
FollowLink(x, y);
} else if (run is IScriptStartRun xse) {
var length = tools.CodeTool.ScriptParser.GetScriptSegmentLength(Model, run.Start);
if (xse is BSERun) length = tools.CodeTool.BattleScriptParser.GetScriptSegmentLength(Model, run.Start);
if (xse is ASERun) length = tools.CodeTool.AnimationScriptParser.GetScriptSegmentLength(Model, run.Start);
SelectionStart = scroll.DataIndexToViewPoint(run.Start);
SelectionEnd = scroll.DataIndexToViewPoint(run.Start + length - 1);
tools.CodeTool.Mode = CodeMode.Script;
if (xse is BSERun) tools.CodeTool.Mode = CodeMode.BattleScript;
if (xse is ASERun) tools.CodeTool.Mode = CodeMode.AnimationScript;
tools.SelectedIndex = tools.IndexOf(tools.CodeTool);
} else {
SelectionStart = scroll.DataIndexToViewPoint(run.Start);
SelectionEnd = scroll.DataIndexToViewPoint(run.Start + run.Length - 1);
}
if (SelectionStart == SelectionEnd && scroll.ViewPointToDataIndex(SelectionStart) >= run.Start && scroll.ViewPointToDataIndex(SelectionEnd) < run.Start + run.Length) {
SelectionStart = scroll.DataIndexToViewPoint(run.Start);
SelectionEnd = scroll.DataIndexToViewPoint(run.Start + run.Length - 1);
}
}
public void ConsiderReload(IFileSystem fileSystem) {
if (!history.IsSaved) return; // don't overwrite local changes
try {
var file = fileSystem.LoadFile(FileName);
if (file == null) return; // asked to load the file, but the file wasn't found... carry on
var metadata = fileSystem.MetadataFor(FileName);
Model.Load(file.Contents, metadata != null ? new StoredMetadata(metadata) : null);
scroll.DataLength = Model.Count;
CascadeScripts();
RefreshBackingData();
// if the new file is shorter, selection might need to be updated
// this forces it to be re-evaluated.
SelectionStart = SelectionStart;
} catch (IOException) {
// something happened when we tried to load the file
// try again soon.
RequestDelayedWork?.Invoke(this, () => ConsiderReload(fileSystem));
}
}
public virtual void FindAllSources(int x, int y) {
var anchor = this[x, y].Format as Anchor;
if (anchor == null) return;
var title = string.IsNullOrEmpty(anchor.Name) ? (y * Width + x + scroll.DataIndex).ToString("X6") : anchor.Name;
title = "Sources of " + title;
var newTab = new SearchResultsViewPort(title);
foreach (var source in anchor.Sources) newTab.Add(CreateChildView(source, source), source, source);
RequestTabChange(this, newTab);
RequestMenuClose?.Invoke(this, EventArgs.Empty);
}
public void OpenSearchResultsTab(string title, IReadOnlyList<(int start, int end)> matches) {
if (matches.Count == 1) {
var (start, end) = matches[0];
selection.GotoAddress(start);
SelectionStart = scroll.DataIndexToViewPoint(start);
SelectionEnd = scroll.DataIndexToViewPoint(end);
return;
}
var newTab = new SearchResultsViewPort(title);
foreach (var (start, end) in matches) newTab.Add(CreateChildView(start, end), start, end);
RequestTabChange(this, newTab);
}
public void OpenDexReorderTab(string dexTableName) {
var newTab = new DexReorderTab(Name, history, Model, dexTableName, HardcodeTablesModel.DexInfoTableName, dexTableName == HardcodeTablesModel.NationalDexTableName);
RequestTabChange(this, newTab);
}
public void OpenImageEditorTab(int address, int spritePage, int palettePage) {
var newTab = new ImageEditorViewModel(history, Model, address, Save) {
SpritePage = spritePage,
PalettePage = palettePage,
};
RequestTabChange(this, newTab);
}
public void ValidateMatchedWords() {
// TODO if this is too slow, add a method to the model to get the set of only MatchedWordRuns.
for (var run = Model.GetNextRun(0); run != NoInfoRun.NullRun; run = Model.GetNextRun(run.Start + Math.Max(1, run.Length))) {
if (!(run is WordRun wordRun)) continue;
var address = Model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, wordRun.SourceArrayName);
if (address == Pointer.NULL) continue;
var array = Model.GetNextRun(address) as ArrayRun;
if (array == null) continue;
var actualValue = Model.ReadValue(wordRun.Start);
if (array.ElementCount == actualValue) continue;
OnMessage?.Invoke(this, $"MatchedWord at {wordRun.Start:X6} was expected to have value {array.ElementCount}, but was {actualValue}.");
Goto.Execute(wordRun.Start.ToString("X6"));
break;
}
}
public void InsertPalette16() {
var selectionPoint = scroll.ViewPointToDataIndex(SelectionStart);
var run1 = Model.GetNextRun(selectionPoint);
var run2 = Model.GetNextRun(selectionPoint + 1);
if (run1.Start + 32 != run2.Start || !(run1 is NoInfoRun)) {
OnError?.Invoke(this, "Palettes insertion requires a no-format anchor with exactly 32 bytes of space.");
return;
}
for (int i = 0; i < 16; i++) {
if (Model.ReadMultiByteValue(run1.Start + i * 2, 2) >= 0x8000) {
OnError?.Invoke(this, $"Palette colors only use 15 bits, but the high bit it set at {run1.Start + i * 2 + 1:X6}.");
return;
}
}
var currentName = Model.GetAnchorFromAddress(-1, run1.Start);
if (string.IsNullOrEmpty(currentName)) currentName = $"{HardcodeTablesModel.DefaultPaletteNamespace}.{run1.Start:X6}";
Model.ObserveAnchorWritten(CurrentChange, currentName, new PaletteRun(run1.Start, new PaletteFormat(4, 1)));
Refresh();
UpdateToolsFromSelection(run1.Start);
}
public void CascadeScript(int address) {
Width = Math.Max(Width, 16); // hack to make the width right on initial load
Height = Math.Max(Height, 16); // hack to make the height right on initial load
var addressText = address.ToString("X6");
Goto.Execute(addressText);
Debug.Assert(scroll.DataIndex == address - address % 16);
var length = tools.CodeTool.ScriptParser.GetScriptSegmentLength(Model, address);
Model.ClearFormat(CurrentChange, address, length - 1);
using (ModelCacheScope.CreateScope(Model)) {
tools.CodeTool.ScriptParser.FormatScript(CurrentChange, Model, address);
}
SelectionStart = scroll.DataIndexToViewPoint(address);
SelectionEnd = scroll.DataIndexToViewPoint(address + length - 1);
tools.CodeTool.Mode = CodeMode.Script;
tools.SelectedIndex = tools.IndexOf(tools.CodeTool);
}
private void Edit(char input) {
var point = GetEditPoint();
var element = this[point.X, point.Y];
if (input.IsAny('\r', '\n')) {
input = ' '; // handle multiline pasting by just treating the newlines as standard whitespace.
withinComment = false;
if (element.Format is PCS || element.Format is ErrorPCS) return; // exit early: newlines within strings are ignored, because they're escaped.
}
if (element.Format is UnderEdit && input == ',') input = ' ';
if (!ShouldAcceptInput(point, element, input)) {
ClearEdits(point);
return;
}
SelectionStart = point;
if (element == this[point.X, point.Y]) {
UnderEdit newFormat;
if (element.Format is UnderEdit underEdit && underEdit.AutocompleteOptions != null) {
var newText = underEdit.CurrentText + input;
var autoCompleteOptions = GetAutocompleteOptions(underEdit.OriginalFormat, element.Value, newText);
newFormat = new UnderEdit(underEdit.OriginalFormat, newText, underEdit.EditWidth, autoCompleteOptions);
} else {
newFormat = element.Format.Edit(input.ToString());
}
currentView[point.X, point.Y] = new HexElement(element, newFormat);
} else {
// ShouldAcceptInput already did the work: nothing to change
}
if (!TryCompleteEdit(point)) {
// only need to notify collection changes if we didn't complete an edit
NotifyCollectionChanged(ResetArgs);
}
}
private IReadOnlyList GetAutocompleteOptions(IDataFormat originalFormat, byte value, string newText, int selectedIndex = -1) {
using (ModelCacheScope.CreateScope(Model)) {
var visitor = new AutocompleteCell(Model, newText, selectedIndex);
(originalFormat ?? Undefined.Instance).Visit(visitor, value);
return visitor.Result;
}
}
private void ClearEdits(Point point) {
if (this[point.X, point.Y].Format is UnderEdit) RefreshBackingData();
withinComment = false;
}
private Point GetEditPoint() {
var selectionStart = scroll.ViewPointToDataIndex(SelectionStart);
var selectionEnd = scroll.ViewPointToDataIndex(SelectionEnd);
var leftEdge = Math.Min(selectionStart, selectionEnd);
var point = scroll.DataIndexToViewPoint(leftEdge);
scroll.ScrollToPoint(ref point);
return point;
}
private void IsTextExecuted(object notUsed) {
var selectionStart = scroll.ViewPointToDataIndex(selection.SelectionStart);
var selectionEnd = scroll.ViewPointToDataIndex(selection.SelectionEnd);
var left = Math.Min(selectionStart, selectionEnd);
var length = Math.Abs(selectionEnd - selectionStart) + 1;
var startPlaces = Model.FindPossibleTextStartingPlaces(left, length);
// do the actual search now that we know places to start
var foundCount = Model.ConsiderResultsAsTextRuns(history.CurrentChange, startPlaces);
if (foundCount == 0) {
OnError?.Invoke(this, "Failed to automatically find text at that location.");
} else {
RefreshBackingData();
}
RequestMenuClose?.Invoke(this, EventArgs.Empty);
}
private bool ShouldAcceptInput(Point point, HexElement element, char input) {
var memoryLocation = scroll.ViewPointToDataIndex(point);
// special cases: if there's no edit unde way, there's a few formats that can be added anywhere. Handle those first.
if (!(element.Format is UnderEdit)) {
if (input == ExtendArray) {
var index = scroll.ViewPointToDataIndex(point);
if (Model.IsAtEndOfArray(index, out var _)) return true;
}
if (input == AnchorStart || input == GotoMarker) {
// anchor edits are actually 0 length
// but lets give them 4 spaces to work with
PrepareForMultiSpaceEdit(point, 4);
var autoCompleteOptions = input == GotoMarker ? new AutoCompleteSelectionItem[0] : null;
var underEdit = new UnderEdit(element.Format, input.ToString(), 4, autoCompleteOptions);
currentView[point.X, point.Y] = new HexElement(element, underEdit);
return true;
}
if (input == CommentStart) {
var underEdit = new UnderEdit(element.Format, input.ToString());
currentView[point.X, point.Y] = new HexElement(element, underEdit);
withinComment = true;
return true;
}
}
// normal case: the logic for how to handle this edit depends on what format is in this cell.
var startCellEdit = new StartCellEdit(Model, memoryLocation, input);
element.Format.Visit(startCellEdit, element.Value);
if (startCellEdit.NewFormat != null) {
// if the edit provided a new format, go ahead and build a new element based on that format.
// if no new format was provided, then the default logic in the method above will make a new UnderEdit cell if the Result is true.
currentView[point.X, point.Y] = new HexElement(element, startCellEdit.NewFormat);
if (startCellEdit.NewFormat.EditWidth > 1) PrepareForMultiSpaceEdit(point, startCellEdit.NewFormat.EditWidth);
}
return startCellEdit.Result;
}
private (Point start, Point end) GetSelectionSpan(Point p) {
var index = scroll.ViewPointToDataIndex(p);
var run = Model.GetNextRun(index);
if (run.Start > index) return (p, p);
(Point, Point) pair(int start, int end) => (scroll.DataIndexToViewPoint(start), scroll.DataIndexToViewPoint(end));
var format = run.CreateDataFormat(Model, index);
while (format is IDataFormatDecorator decorator) format = decorator.OriginalFormat;
if (format is IDataFormatInstance instance) {
return pair(instance.Source, instance.Source + instance.Length - 1);
}
if (!(run is ITableRun array)) return (p, p);
var naturalEnd = array.Start + array.ElementCount * array.ElementLength;
if (naturalEnd <= index) {
return pair(naturalEnd, array.Start + array.Length - 1);
}
var offset = array.ConvertByteOffsetToArrayOffset(index);
var type = array.ElementContent[offset.SegmentIndex].Type;
if (type == ElementContentType.Pointer || type == ElementContentType.Integer || type == ElementContentType.BitArray) {
return pair(offset.SegmentStart, offset.SegmentStart + array.ElementContent[offset.SegmentIndex].Length - 1);
}
return (p, p);
}
private void PrepareForMultiSpaceEdit(Point point, int length) {
var index = scroll.ViewPointToDataIndex(point);
var endIndex = index + length - 1;
for (int i = 1; i < length; i++) {
point = scroll.DataIndexToViewPoint(index + i);
if (point.Y >= Height) return;
var element = this[point.X, point.Y];
var newFormat = element.Format.Edit(string.Empty);
currentView[point.X, point.Y] = new HexElement(element, newFormat);
}
selection.PropertyChanged -= SelectionPropertyChanged; // don't notify on multi-space edit: it breaks up the undo history
SelectionEnd = scroll.DataIndexToViewPoint(endIndex);
selection.PropertyChanged += SelectionPropertyChanged;
}
private bool TryCompleteEdit(Point point) {
// wrap this whole method in an anti-recursion clause
selection.PreviewSelectionStartChanged -= ClearActiveEditBeforeSelectionChanges;
using (new StubDisposable { Dispose = () => selection.PreviewSelectionStartChanged += ClearActiveEditBeforeSelectionChanges }) {
var element = this[point.X, point.Y];
var underEdit = element.Format as UnderEdit;
if (underEdit == null) return false; // no edit to complete
if (TryGeneralCompleteEdit(underEdit.CurrentText, point, out bool result)) {
return result;
}
// normal case: whether or not to accept the edit depends on the existing cell format
var dataIndex = scroll.ViewPointToDataIndex(point);
var completeEditOperation = new CompleteCellEdit(Model, dataIndex, underEdit.CurrentText, history.CurrentChange);
using (ModelCacheScope.CreateScope(Model)) {
(underEdit.OriginalFormat ?? Undefined.Instance).Visit(completeEditOperation, element.Value);
if (completeEditOperation.Result) {
// if the data we just changed was in a table, notify children of that table about the change
if (Model.GetNextRun(dataIndex) is ITableRun tableRun) {
var offsets = tableRun.ConvertByteOffsetToArrayOffset(dataIndex);
var errorInfo = tableRun.NotifyChildren(Model, history.CurrentChange, offsets.ElementIndex, offsets.SegmentIndex);
HandleErrorInfo(errorInfo);
}
// update the cell / selection
if (completeEditOperation.NewCell != null) {
currentView[point.X, point.Y] = completeEditOperation.NewCell;
}
if (completeEditOperation.DataMoved || completeEditOperation.NewDataIndex > scroll.DataLength) scroll.DataLength = Model.Count;
// update tools from the new moved selection
var run = Model.GetNextRun(completeEditOperation.NewDataIndex);
if (run.Start > completeEditOperation.NewDataIndex) run = new NoInfoRun(Model.Count);
if (completeEditOperation.DataMoved) UpdateToolsFromSelection(run.Start);
if (run is ITableRun) {
Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged);
}
if (run is ITableRun || run is IStreamRun) Tools.Schedule(Tools.StringTool.DataForCurrentRunChanged);
if (run is ISpriteRun || run is IPaletteRun) {
tools.Schedule(tools.SpriteTool.DataForCurrentRunChanged);
tools.Schedule(tools.TableTool.DataForCurrentRunChanged);
}
if (completeEditOperation.MessageText != null) OnMessage?.Invoke(this, completeEditOperation.MessageText);
if (completeEditOperation.ErrorText != null) OnError?.Invoke(this, completeEditOperation.ErrorText);
// refresh the screen
if (!SilentScroll(completeEditOperation.NewDataIndex) && completeEditOperation.NewCell == null) {
RefreshBackingData();
}
UpdateToolsFromSelection(completeEditOperation.NewDataIndex);
}
}
return completeEditOperation.Result;
}
}
///
/// 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) {
result = false;
// goto marker
if (currentText.StartsWith(GotoMarker.ToString())) {
if (currentText.Length == 2 && currentText[1] == '{') {
var currentAddress = scroll.ViewPointToDataIndex(point);
var destination = Model.ReadPointer(currentAddress);
if (destination == Pointer.NULL) {
if (CreateNewData(currentAddress)) {
destination = Model.ReadPointer(currentAddress);
} else {
OnError?.Invoke(this, $"Could not jump using pointer at {currentAddress:X6}");
}
}
ClearEdits(point);
if (destination >= 0 && destination < Model.Count) {
using (ChangeHistory.ContinueCurrentTransaction()) {
Goto.Execute(destination);
selection.SetJumpBackPoint(currentAddress + 4);
}
} else if (destination != Pointer.NULL) {
OnError?.Invoke(this, $"Could not jump using pointer at {currentAddress:X6}");
}
RequestMenuClose?.Invoke(this, EventArgs.Empty);
result = true;
} else if (currentText.Length == 2 && currentText[1] == '}') {
ClearEdits(point);
using (ChangeHistory.ContinueCurrentTransaction()) selection.Back.Execute();
RequestMenuClose?.Invoke(this, EventArgs.Empty);
result = true;
}
if (char.IsWhiteSpace(currentText[currentText.Length - 1])) {
var destination = currentText.Substring(1).Trim();
ClearEdits(point);
if (currentText.Contains("=")) {
UpdateConstant(destination);
} else {
var parts = destination.Split(CommandMarker);
if (!string.IsNullOrWhiteSpace(parts[0])) Goto.Execute(parts[0]);
for (int i = 1; i < parts.Length; i++) ExecuteMetacommand(parts[i]);
}
RequestMenuClose?.Invoke(this, EventArgs.Empty);
result = true;
}
return true;
}
// comment
if (currentText.StartsWith(CommentStart.ToString())) {
if (currentText.Length > 1 && currentText.EndsWith(CommentStart.ToString())) withinComment = false;
result = (currentText.EndsWith(" ") || currentText.EndsWith("#")) && !withinComment;
if (result) ClearEdits(point);
return true;
}
// anchor start
if (currentText.StartsWith(AnchorStart.ToString())) {
TryUpdate(ref anchorText, currentText, nameof(AnchorText));
var endingCharacter = currentText[currentText.Length - 1];
// anchor format will only end once the user
// -> types a whitespace character,
// -> types a closing quote for the text format ""
// -> types a closing ` for a `` format
if (!char.IsWhiteSpace(endingCharacter) && !currentText.EndsWith(AsciiRun.StreamDelimeter.ToString()) && !currentText.EndsWith(PCSRun.StringDelimeter.ToString())) {
AnchorTextVisible = true;
return true;
}
// special case: `asc` has a length token outside the ``, so the anchor isn't completed if it ends with `asc`
if (currentText.EndsWith("`asc`")) {
AnchorTextVisible = true;
return true;
}
// only end the anchor edit if the [] brace count matches
if (currentText.Sum(c => c == '[' ? 1 : c == ']' ? -1 : 0) != 0) {
AnchorTextVisible = true;
return true;
}
// only end the anchor if the "" and `` quote count are even
if (currentText.Count(AsciiRun.StreamDelimeter) % 2 != 0 || currentText.Count(StringDelimeter) % 2 != 0) {
AnchorTextVisible = true;
return true;
}
if (!CompleteAnchorEdit(point)) exitEditEarly = true;
result = true;
return true;
}
// directive marker
var element = this[point.X, point.Y];
var underEdit = element.Format as UnderEdit;
if (currentText.StartsWith(DirectiveMarker.ToString()) && currentText.Count(c => c == DirectiveMarker) == 1) {
if (underEdit.OriginalFormat is PCS || underEdit.OriginalFormat is Ascii) {
// if we're in a text cell, don't allow directives.
} else {
result = CompleteDirectiveEdit(point, currentText);
return true;
}
}
// 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, 1, ref arrayRun);
if (!errorInfo.HasError || errorInfo.IsWarning) {
if (arrayRun != null && arrayRun.Start != originalArray.Start) {
ScrollFromTableMove(dataIndex, originalArray, arrayRun);
}
RefreshBackingData();
SelectionEnd = GetSelectionSpan(SelectionStart).end;
}
HandleErrorInfo(errorInfo);
result = true;
return true;
}
return false;
}
private void UpdateConstant(string expression) {
var parts = expression.Split('=');
if (parts.Length != 2) {
RaiseError("Could not parse constant assignment expression.");
return;
}
if (!int.TryParse(parts[1], out var value)) {
RaiseError("Could not parse constant assignment expression.");
return;
}
var locations = Model.GetMatchedWords(parts[0]);
if (locations == null || locations.Count == 0) {
RaiseError($"{parts[0]} is not a named constant!");
return;
}
foreach (var address in locations) {
if (!(Model.GetNextRun(address) is WordRun currentRun)) continue;
var writeValue = value + currentRun.ValueOffset;
if (writeValue < 0 || writeValue > 255) {
RaiseError($"{currentRun.Start:X6}: value out of range!");
}
Model.WriteMultiByteValue(address, currentRun.Length, CurrentChange, writeValue);
}
}
///
/// Current Available metacommands:
/// lz(1024) -> write 1024 compressed bytes. Error if we're not in freespace, do nothing if we're at lz data of the expected length already.
/// 00(32) -> Write 32 bytes of zero. Error if we're not in freespace (FF).
/// * Does not error if clearing a subset (or entire) table, so long as the clear matches a multiple of a row length. Still clears that data though.
/// * Does not error if clearing exactly the length of a non-table run. Don't clear the data either: all 0's may not be valid (such as with strings)
/// put(1234)-> put the bytes 12, then 34, at the current location, but don't change the current selection.
/// works no matter what the current data is.
///
private void ExecuteMetacommand(string command) {
command = command.ToLower();
var index = scroll.ViewPointToDataIndex(SelectionStart);
var paramsStart = command.IndexOf("(");
var paramsEnd = command.IndexOf(")");
var length = 0;
if (command.StartsWith("lz(") && paramsEnd > 3 && int.TryParse(command.Substring(3, paramsEnd - 3), out length)) {
// only do the write if the current data isn't compressed data of the right length
var existingCompressedData = LZRun.Decompress(Model, index);
var newCompressed = LZRun.Compress(new byte[length], 0, length);
if (existingCompressedData != null && existingCompressedData.Length == length) {
// do nothing, it's already the right data type
} else if (newCompressed.Count.Range().All(i => Model[index + i] == 0xFF)) {
// data is all FF, go ahead and write
for (int i = 0; i < newCompressed.Count; i++) CurrentChange.ChangeData(Model, index + i, newCompressed[i]);
} else {
// data is not freespace and is not the correct data: error
RaiseError($"Writing {length} compressed bytes would overwrite existing data.");
exitEditEarly = true;
}
} else if (command.StartsWith("00(") && paramsEnd > 3 && int.TryParse(command.Substring(3, paramsEnd - 3), out length)) {
var currentRun = Model.GetNextRun(index);
var tableRun = currentRun as ITableRun;
if (tableRun != null && currentRun.Start == index && length % tableRun.ElementLength == 0 && length <= tableRun.Length) {
// we're trying to clear out table data.
// assume that the user wanted us to clear it.
// do NOT do the clear if the current clear is bigger than the current table: that could wipe existing data.
ClearPointersFromTable(tableRun, index, length);
for (int i = 0; i < length; i++) CurrentChange.ChangeData(Model, index + i, 0);
} else if (tableRun == null && currentRun.Start == index && currentRun.Length == length) {
// we're trying to clear out a non-table
// the length matches exactly, so we shouldn't error. But also, don't actually clear.
} else if (length.Range().All(i => Model[index + i] == 0xFF)) {
for (int i = 0; i < length; i++) CurrentChange.ChangeData(Model, index + i, 0);
} else {
RaiseError($"Writing {length} 00 bytes would overwrite existing data.");
exitEditEarly = true;
}
} else if (command.StartsWith("game(") && paramsEnd > 5) {
var content = command.Substring(5, paramsEnd - 5).ToLower();
var gameCode = Model.GetGameCode().ToLower();
if (content == "all") {
// all good
} else if (!content.Contains(gameCode)) {
skipToNextGameCode = true;
}
} else if (command.StartsWith("put(") && paramsEnd > 4) {
var content = command.Substring(4, paramsEnd - 4);
if (content.Length % 2 != 0 || !content.All(AllHexCharacters.Contains)) {
RaiseError("'put' expects hex bytes as an argument. ");
exitEditEarly = true;
return;
}
for (int i = 0; i < content.Length / 2; i++) {
var data = byte.Parse(content.Substring(i * 2, 2), NumberStyles.HexNumber);
CurrentChange.ChangeData(Model, index + i, data);
}
} else {
RaiseError($"Could not parse metacommand {command}.");
exitEditEarly = true;
}
}
private void ClearPointersFromTable(ITableRun tableRun, int index, int length) {
foreach (var segment in tableRun.ElementContent) {
if (segment.Type != ElementContentType.Pointer) continue;
var offset = tableRun.ElementContent.Until(seg => seg == segment).Sum(seg => seg.Length);
for (int i = offset; i < length; i += tableRun.ElementLength) {
var destination = Model.ReadPointer(tableRun.Start + i);
Model.ClearPointer(CurrentChange, tableRun.Start + i, destination);
}
}
}
/// True if it was completed successfully, false if some sort of error occurred and we should abort the remainder of the edit.
private bool CompleteAnchorEdit(Point point) {
var underEdit = (UnderEdit)this[point.X, point.Y].Format;
var index = scroll.ViewPointToDataIndex(point);
ErrorInfo errorInfo;
// if it's an unnamed text/stream anchor, we have special logic for that
using (ModelCacheScope.CreateScope(Model)) {
if (underEdit.CurrentText.Trim() == AnchorStart + PCSRun.SharedFormatString) {
int count = Model.ConsiderResultsAsTextRuns(history.CurrentChange, new[] { index });
if (count == 0) {
errorInfo = new ErrorInfo("An anchor with nothing pointing to it must have a name.");
} else {
errorInfo = ErrorInfo.NoError;
}
} else if (underEdit.CurrentText == AnchorStart + PLMRun.SharedFormatString) {
if (!PokemonModel.ConsiderAsPlmStream(Model, index, history.CurrentChange)) {
errorInfo = new ErrorInfo("An anchor with nothing pointing to it must have a name.");
} else {
errorInfo = ErrorInfo.NoError;
Tools.StringTool.RefreshContentAtAddress();
}
} else if (underEdit.CurrentText == AnchorStart + XSERun.SharedFormatString) {
// TODO
CascadeScript(index);
errorInfo = ErrorInfo.NoError;
} else {
errorInfo = PokemonModel.ApplyAnchor(Model, history.CurrentChange, index, underEdit.CurrentText);
Tools.StringTool.RefreshContentAtAddress();
}
}
ClearEdits(point);
UpdateToolsFromSelection(index);
if (errorInfo == ErrorInfo.NoError) {
if (Model.GetNextRun(index) is ArrayRun array && array.Start == index) Goto.Execute(index.ToString("X2"));
return true;
}
HandleErrorInfo(errorInfo);
return errorInfo.IsWarning;
}
private bool CompleteDirectiveEdit(Point point, string currentText) {
currentText = currentText.ToLower();
if (!currentText.EndsWith(" ")) return false;
if (currentText.StartsWith(".align ") && currentText.Length > 8) {
if (!int.TryParse(currentText.Substring(7), out int value)) value = 4;
ClearEdits(point);
var index = scroll.ViewPointToDataIndex(point);
if (value == 2) SelectionStart = scroll.DataIndexToViewPoint(index + point.X % 2);
if (value == 4 && point.X % 4 != 0) SelectionStart = scroll.DataIndexToViewPoint(index + 4 - (point.X % 4));
}
if (currentText.StartsWith(".text")) ClearEdits(point);
if (currentText.StartsWith(".thumb")) ClearEdits(point);
return false;
}
private void ScrollFromTableMove(int initialSelection, ITableRun oldRun, ITableRun newRun) {
scroll.DataLength = Model.Count; // possible length change
var tableOffset = scroll.DataIndex - oldRun.Start;
var relativeSelection = initialSelection - oldRun.Start;
selection.PropertyChanged -= SelectionPropertyChanged;
selection.GotoAddress(newRun.Start + tableOffset);
selection.SelectionStart = scroll.DataIndexToViewPoint(newRun.Start + relativeSelection);
selection.PropertyChanged += SelectionPropertyChanged;
}
private bool SilentScroll(int memoryLocation) {
var nextPoint = scroll.DataIndexToViewPoint(memoryLocation);
var didScroll = true;
if (!scroll.ScrollToPoint(ref nextPoint)) {
// only need to notify collection change if we didn't auto-scroll after changing cells
NotifyCollectionChanged(ResetArgs);
didScroll = false;
}
UpdateSelectionWithoutNotify(nextPoint);
return didScroll;
}
public void HandleErrorInfo(ErrorInfo info) {
if (!info.HasError) return;
if (info.IsWarning) OnMessage?.Invoke(this, info.ErrorMessage);
else OnError?.Invoke(this, info.ErrorMessage);
}
///
/// When automatically updating the selection,
/// update it without notifying ourselves.
/// This lets us tell the difference between a manual cell change and an auto-cell change,
/// which is useful for deciding change history boundaries.
///
private void UpdateSelectionWithoutNotify(Point nextPoint) {
selection.PropertyChanged -= SelectionPropertyChanged;
SelectionStart = nextPoint;
NotifyPropertyChanged(nameof(SelectionStart));
NotifyPropertyChanged(nameof(SelectionEnd));
selection.PropertyChanged += SelectionPropertyChanged;
}
private void ModelChangedByTool(object sender, IFormattedRun run) {
if (run.Start < scroll.ViewPointToDataIndex(new Point(Width - 1, Height - 1)) || run.Start + run.Length > scroll.DataIndex) {
// there's some visible data that changed
RefreshBackingData();
}
if (run is ITableRun && sender != Tools.StringTool && Model.GetNextRun(Tools.StringTool.Address).Start == run.Start) Tools.StringTool.DataForCurrentRunChanged();
if (run is ITableRun && Model.GetNextRun(Tools.TableTool.Address).Start == run.Start) Tools.TableTool.DataForCurrentRunChanged();
}
private void ModelDataMovedByTool(object sender, (int originalLocation, int newLocation) locations) {
scroll.DataLength = Model.Count;
if (scroll.DataIndex <= locations.originalLocation && locations.originalLocation < scroll.ViewPointToDataIndex(new Point(Width - 1, Height - 1))) {
// data was moved from onscreen: follow it
int offset = locations.originalLocation - scroll.DataIndex;
selection.GotoAddress(locations.newLocation - offset);
}
RaiseMessage($"Data was automatically moved to {locations.newLocation:X6}. Pointers were updated.");
}
private void ModelChangedByCodeTool(object sender, ErrorInfo e) {
RefreshBackingData();
HandleErrorInfo(e);
}
private void RefreshBackingData(Point p) {
var index = scroll.ViewPointToDataIndex(p);
var edited = Model.HasChanged(index);
if (index < 0 | index >= Model.Count) { currentView[p.X, p.Y] = HexElement.Undefined; return; }
var run = Model.GetNextRun(index);
if (index < run.Start) { currentView[p.X, p.Y] = new HexElement(Model[index], edited, None.Instance); return; }
var format = run.CreateDataFormat(Model, index);
format = Model.WrapFormat(run, format, index);
currentView[p.X, p.Y] = new HexElement(Model[index], edited, format);
}
private void RefreshBackingData() {
currentView = new HexElement[Width, Height];
RequestMenuClose?.Invoke(this, EventArgs.Empty);
NotifyCollectionChanged(ResetArgs);
NotifyPropertyChanged(nameof(FreeSpaceStart));
}
private void RefreshBackingDataFull() {
currentView = new HexElement[Width, Height];
IFormattedRun run = null;
using (ModelCacheScope.CreateScope(Model)) {
for (int y = 0; y < Height; y++) {
for (int x = 0; x < Width; x++) {
var index = scroll.ViewPointToDataIndex(new Point(x, y));
var edited = Model.HasChanged(index);
if (run == null || index >= run.Start + run.Length) {
run = Model.GetNextRun(index) ?? new NoInfoRun(Model.Count);
}
if (index < 0 || index >= Model.Count) {
currentView[x, y] = HexElement.Undefined;
} else if (index >= run.Start) {
var format = run is BaseRun baseRun ? baseRun.CreateDataFormat(Model, index, x == 0, Width) : run.CreateDataFormat(Model, index);
format = Model.WrapFormat(run, format, index);
currentView[x, y] = new HexElement(Model[index], edited, format);
} else {
currentView[x, y] = new HexElement(Model[index], edited, None.Instance);
}
}
}
}
}
private void UpdateColumnHeaders() {
var index = scroll.ViewPointToDataIndex(new Point(0, 0));
var run = Model.GetNextRun(index) as ArrayRun;
if (run != null && run.Start > index) run = null; // only use the run if it starts _before_ the screen
var headers = run?.GetColumnHeaders(Width, index) ?? HeaderRow.GetDefaultColumnHeaders(Width, index);
for (int i = 0; i < headers.Count; i++) {
if (i < ColumnHeaders.Count) ColumnHeaders[i] = headers[i];
else ColumnHeaders.Add(headers[i]);
}
while (ColumnHeaders.Count > headers.Count) ColumnHeaders.RemoveAt(ColumnHeaders.Count - 1);
}
private void NotifyCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args);
}
}