From ebc9d1f33db109d71fa94e9c5857c80d2f77db9e Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Mon, 29 Apr 2019 16:32:14 -0500 Subject: [PATCH] first part of autocomplete is in mostly the tests need to make the pointers actually fill the model need to make the UI still need to handle enums still --- src/HexManiac.Core/HexManiac.Core.csproj | 1 + src/HexManiac.Core/Models/AutoSearchModel.cs | 2 +- src/HexManiac.Core/Models/StoredMetadata.cs | 2 + .../ViewModels/AutoCompleteSelectionItem.cs | 33 ++++++++ src/HexManiac.Core/ViewModels/DataFormats.cs | 14 +++- .../ViewModels/GotoControlViewModel.cs | 26 +------ .../ViewModels/SearchResultsViewPort.cs | 4 + src/HexManiac.Core/ViewModels/Theme.cs | 5 +- .../ViewModels/Tools/TableTool.cs | 2 + src/HexManiac.Core/ViewModels/ViewPort.cs | 22 +++++- src/HexManiac.Tests/AutoSearchTests.cs | 3 +- src/HexManiac.Tests/ChangeHistoryTests.cs | 2 + src/HexManiac.Tests/HexManiac.Tests.csproj | 1 + .../ViewPortAutocompleteEditTests.cs | 77 +++++++++++++++++++ src/HexManiac.WPF/Windows/MainWindow.xaml | 3 +- src/HexManiac.WPF/Windows/ThemeSelector.xaml | 4 +- 16 files changed, 164 insertions(+), 37 deletions(-) create mode 100644 src/HexManiac.Core/ViewModels/AutoCompleteSelectionItem.cs create mode 100644 src/HexManiac.Tests/ViewPortAutocompleteEditTests.cs diff --git a/src/HexManiac.Core/HexManiac.Core.csproj b/src/HexManiac.Core/HexManiac.Core.csproj index 7732781a..7ad61814 100644 --- a/src/HexManiac.Core/HexManiac.Core.csproj +++ b/src/HexManiac.Core/HexManiac.Core.csproj @@ -53,6 +53,7 @@ + diff --git a/src/HexManiac.Core/Models/AutoSearchModel.cs b/src/HexManiac.Core/Models/AutoSearchModel.cs index b14de645..4e086a2a 100644 --- a/src/HexManiac.Core/Models/AutoSearchModel.cs +++ b/src/HexManiac.Core/Models/AutoSearchModel.cs @@ -25,7 +25,7 @@ namespace HavenSoft.HexManiac.Core.Models { public override int EarliestAllowedAnchor => 0x200; public AutoSearchModel(byte[] data, StoredMetadata metadata = null) : base(data, metadata) { - if (metadata != null) return; + if (metadata != null && !metadata.IsEmpty) return; gameCode = string.Concat(Enumerable.Range(0xAC, 4).Select(i => ((char)data[i]).ToString())); diff --git a/src/HexManiac.Core/Models/StoredMetadata.cs b/src/HexManiac.Core/Models/StoredMetadata.cs index 7adb18d5..13824e6c 100644 --- a/src/HexManiac.Core/Models/StoredMetadata.cs +++ b/src/HexManiac.Core/Models/StoredMetadata.cs @@ -8,6 +8,8 @@ namespace HavenSoft.HexManiac.Core.Models { public IReadOnlyList NamedAnchors { get; } public IReadOnlyList UnmappedPointers { get; } + public bool IsEmpty => NamedAnchors.Count == 0 && UnmappedPointers.Count == 0; + public StoredMetadata(IReadOnlyList anchors, IReadOnlyList unmappedPointers) { NamedAnchors = anchors; UnmappedPointers = unmappedPointers; diff --git a/src/HexManiac.Core/ViewModels/AutoCompleteSelectionItem.cs b/src/HexManiac.Core/ViewModels/AutoCompleteSelectionItem.cs new file mode 100644 index 00000000..ba591f89 --- /dev/null +++ b/src/HexManiac.Core/ViewModels/AutoCompleteSelectionItem.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace HavenSoft.HexManiac.Core.ViewModels { + public class AutoCompleteSelectionItem : IEquatable, INotifyPropertyChanged { + public string CompletionText { get; } + public bool IsSelected { get; } + +#pragma warning disable 0067 // it's ok if events are never used after implementing an interface + public event PropertyChangedEventHandler PropertyChanged; +#pragma warning restore 0067 + + public AutoCompleteSelectionItem(string text, bool selection) => (CompletionText, IsSelected) = (text, selection); + + public static IReadOnlyList Generate(IEnumerable options, int selectionIndex) { + var list = new List(); + + int i = 0; + foreach (var option in options) { + list.Add(new AutoCompleteSelectionItem(option, i == selectionIndex)); + i++; + } + + return list; + } + + public bool Equals(AutoCompleteSelectionItem other) { + if (other == null) return false; + return IsSelected == other.IsSelected && CompletionText == other.CompletionText; + } + } +} diff --git a/src/HexManiac.Core/ViewModels/DataFormats.cs b/src/HexManiac.Core/ViewModels/DataFormats.cs index d3bf1fe1..7dc38456 100644 --- a/src/HexManiac.Core/ViewModels/DataFormats.cs +++ b/src/HexManiac.Core/ViewModels/DataFormats.cs @@ -53,7 +53,13 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public IDataFormat OriginalFormat { get; } public string CurrentText { get; } public int EditWidth { get; } - public UnderEdit(IDataFormat original, string text, int editWidth = 1) => (OriginalFormat, CurrentText, EditWidth) = (original, text, editWidth); + public IReadOnlyList AutocompleteOptions { get; } + public UnderEdit(IDataFormat original, string text, int editWidth = 1, IReadOnlyList autocompleteOptions = null) { + OriginalFormat = original; + CurrentText = text; + EditWidth = editWidth; + AutocompleteOptions = autocompleteOptions; + } public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); public bool Equals(IDataFormat format) { @@ -61,6 +67,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { if (!OriginalFormat.Equals(that.OriginalFormat)) return false; if (EditWidth != that.EditWidth) return false; + if (AutocompleteOptions != null ^ that.AutocompleteOptions != null) return false; // if only one is null, not equal + if (AutocompleteOptions != null && that.AutocompleteOptions != null && AutocompleteOptions.SequenceEqual(that.AutocompleteOptions)) return false; return CurrentText == that.CurrentText; } } @@ -190,7 +198,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public Integer(int source, int position, int value, int length) => (Source, Position, Value, Length) = (source, position, value, length); - public bool Equals(IDataFormat other) { + public virtual bool Equals(IDataFormat other) { if (!(other is Integer that)) return false; return Source == that.Source && Position == that.Position && Value == that.Value && Length == that.Length; } @@ -206,7 +214,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public new string Value { get; } public IntegerEnum(int source, int position, string value, int length) : base(source, position, -1, length) => Value = value; - public bool Equals(IDataFormat other) { + public override bool Equals(IDataFormat other) { if (!(other is IntegerEnum that)) return false; return Value == that.Value && base.Equals(other); } diff --git a/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs b/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs index 98b28821..5c3bd8ba 100644 --- a/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs +++ b/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs @@ -26,7 +26,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { if (viewPort == null) return; if (TryUpdate(ref text, value)) { var options = viewPort.Model?.GetAutoCompleteAnchorNameOptions(text) ?? new string[0]; - AutoCompleteOptions = CreateAutoCompleteOptions(options, options.Count); + AutoCompleteOptions = AutoCompleteSelectionItem.Generate(options, completionIndex); ShowAutoCompleteOptions = AutoCompleteOptions.Count > 0; } } @@ -37,7 +37,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { get => completionIndex; set { if (TryUpdate(ref completionIndex, value.LimitToRange(-1, autoCompleteOptions.Count - 1))) { - AutoCompleteOptions = CreateAutoCompleteOptions(AutoCompleteOptions.Select(option => option.CompletionText), AutoCompleteOptions.Count); + AutoCompleteOptions = AutoCompleteSelectionItem.Generate(AutoCompleteOptions.Select(option => option.CompletionText), completionIndex); } } } @@ -98,27 +98,5 @@ namespace HavenSoft.HexManiac.Core.ViewModels { Execute = arg => ControlVisible = (bool)arg, }; } - - private IReadOnlyList CreateAutoCompleteOptions(IEnumerable options, int length) { - if (completionIndex >= length) { - completionIndex = length - 1; - NotifyPropertyChanged(nameof(CompletionIndex)); - } - var list = new List(length); - - int i = 0; - foreach (var option in options) { - list.Add(new AutoCompleteSelectionItem(option, i == completionIndex)); - i++; - } - - return list; - } - } - - public class AutoCompleteSelectionItem { - public string CompletionText { get; } - public bool IsSelected { get; } - public AutoCompleteSelectionItem(string text, bool selection) => (CompletionText, IsSelected) = (text, selection); } } diff --git a/src/HexManiac.Core/ViewModels/SearchResultsViewPort.cs b/src/HexManiac.Core/ViewModels/SearchResultsViewPort.cs index f2a094a2..edae503e 100644 --- a/src/HexManiac.Core/ViewModels/SearchResultsViewPort.cs +++ b/src/HexManiac.Core/ViewModels/SearchResultsViewPort.cs @@ -241,8 +241,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels { public TableTool TableTool => null; public IDisposable DeferUpdates => new StubDisposable(); + +#pragma warning disable 0067 // it's ok if events are never used after implementing an interface public event EventHandler OnError; public event PropertyChangedEventHandler PropertyChanged; +#pragma warning restore 0067 + public void Schedule(Action action) => action(); public void RefreshContent() { } diff --git a/src/HexManiac.Core/ViewModels/Theme.cs b/src/HexManiac.Core/ViewModels/Theme.cs index ad646aaa..9df632ec 100644 --- a/src/HexManiac.Core/ViewModels/Theme.cs +++ b/src/HexManiac.Core/ViewModels/Theme.cs @@ -6,7 +6,7 @@ using System.Linq; namespace HavenSoft.HexManiac.Core.ViewModels { public class Theme : ViewModelCore { private string lightColor = "#DDDDDD", darkColor = "#080808"; - private double hueOffset = 0.3, accentSaturation = 0.4, accentValue = 0.7, highlightBrightness = 0.7; + private double hueOffset = 0.1, accentSaturation = 0.9, accentValue = 0.6, highlightBrightness = 0.7; private bool lightVariant; public bool LightVariant { @@ -106,8 +106,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var accent = new List<(double hue, double sat, double bright)>(); var saturation = accentSaturation * .8 + .2; var accentBrightness = accentValue * .6 + .4; - var brightness = hsbLight.bright * accentBrightness + (1 - accentBrightness) * hsbDark.bright; - var prototype = (hue: (hueOffset - .5) / 12, sat: saturation, bright: brightness); + var prototype = (hue: (hueOffset - .5) / 12, sat: saturation, bright: accentBrightness); for (int i = 0; i < 8; i++) { accent.Add(prototype); prototype.hue += 1 / 8.0; diff --git a/src/HexManiac.Core/ViewModels/Tools/TableTool.cs b/src/HexManiac.Core/ViewModels/Tools/TableTool.cs index 127c03d5..32c597db 100644 --- a/src/HexManiac.Core/ViewModels/Tools/TableTool.cs +++ b/src/HexManiac.Core/ViewModels/Tools/TableTool.cs @@ -59,7 +59,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { public event EventHandler ModelDataChanged; +#pragma warning disable 0067 // it's ok if events are never used after implementing an interface public event EventHandler<(int originalLocation, int newLocation)> ModelDataMoved; // invoke when a new item gets added and the table has to move +#pragma warning restore 0067 public TableTool(IDataModel model, Selection selection, ChangeHistory history, IToolTrayViewModel toolTray) { this.model = model; diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index dd23afde..b14bf108 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -867,7 +867,18 @@ namespace HavenSoft.HexManiac.Core.ViewModels { SelectionStart = point; if (element == currentView[point.X, point.Y]) { - var newFormat = element.Format.Edit(input.ToString()); + UnderEdit newFormat; + if (element.Format is UnderEdit underEdit && underEdit.AutocompleteOptions != null) { + if (underEdit.CurrentText.StartsWith(PointerStart.ToString())) { + var newText = underEdit.CurrentText + input; + var autocompleteOptions = GetNewPointerAutocompleteOptions(newText); + newFormat = new UnderEdit(underEdit.OriginalFormat, newText, underEdit.EditWidth, autocompleteOptions); + } else { + throw new NotImplementedException(); + } + } else { + newFormat = element.Format.Edit(input.ToString()); + } currentView[point.X, point.Y] = new HexElement(element.Value, newFormat); } else { // ShouldAcceptInput already did the work: nothing to change @@ -969,7 +980,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels { // if the user tries to edit the pointer but forgets the opening bracket, add it for them. if (input != PointerStart) editText = PointerStart + editText; var newFormat = element.Format.Edit(editText); - newFormat = new UnderEdit(newFormat.OriginalFormat, newFormat.CurrentText, 4); + var autocompleteOptions = GetNewPointerAutocompleteOptions(editText); + newFormat = new UnderEdit(newFormat.OriginalFormat, newFormat.CurrentText, 4, autocompleteOptions); currentView[point.X, point.Y] = new HexElement(element.Value, newFormat); return true; } @@ -1027,6 +1039,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels { return false; } + private IReadOnlyList GetNewPointerAutocompleteOptions(string text) { + if (text.StartsWith(PointerStart.ToString()))text = text.Substring(1); + var options = Model.GetAutoCompleteAnchorNameOptions(text); + return AutoCompleteSelectionItem.Generate(options, -1); + } + private (Point start, Point end) GetSelectionSpan(Point p) { var index = scroll.ViewPointToDataIndex(p); var run = Model.GetNextRun(index); diff --git a/src/HexManiac.Tests/AutoSearchTests.cs b/src/HexManiac.Tests/AutoSearchTests.cs index 08e3033e..7658a89e 100644 --- a/src/HexManiac.Tests/AutoSearchTests.cs +++ b/src/HexManiac.Tests/AutoSearchTests.cs @@ -148,7 +148,8 @@ namespace HavenSoft.HexManiac.Tests { if (modelCache.TryGetValue(name, out var cachedModel)) return cachedModel; Skip.IfNot(File.Exists(name)); var data = File.ReadAllBytes(name); - var model = new AutoSearchModel(data); + var metadata = new StoredMetadata(new string[0]); + var model = new AutoSearchModel(data, metadata); modelCache[name] = model; return model; } diff --git a/src/HexManiac.Tests/ChangeHistoryTests.cs b/src/HexManiac.Tests/ChangeHistoryTests.cs index de2a2172..76f0c59a 100644 --- a/src/HexManiac.Tests/ChangeHistoryTests.cs +++ b/src/HexManiac.Tests/ChangeHistoryTests.cs @@ -9,7 +9,9 @@ using Xunit; namespace HavenSoft.HexManiac.Tests { public class FakeChangeToken : List, IChangeToken { +#pragma warning disable 0067 // it's ok if events are never used after implementing an interface public event EventHandler OnNewDataChange; +#pragma warning restore 0067 public bool HasDataChange => Count > 0; public FakeChangeToken() { } public FakeChangeToken(IEnumerable data) : base(data) { } diff --git a/src/HexManiac.Tests/HexManiac.Tests.csproj b/src/HexManiac.Tests/HexManiac.Tests.csproj index 4f53fbc5..c8cbb7e8 100644 --- a/src/HexManiac.Tests/HexManiac.Tests.csproj +++ b/src/HexManiac.Tests/HexManiac.Tests.csproj @@ -80,6 +80,7 @@ + diff --git a/src/HexManiac.Tests/ViewPortAutocompleteEditTests.cs b/src/HexManiac.Tests/ViewPortAutocompleteEditTests.cs new file mode 100644 index 00000000..82186b91 --- /dev/null +++ b/src/HexManiac.Tests/ViewPortAutocompleteEditTests.cs @@ -0,0 +1,77 @@ +using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.ViewModels; +using HavenSoft.HexManiac.Core.ViewModels.DataFormats; +using System; +using Xunit; + +namespace HavenSoft.HexManiac.Tests { + public class ViewPortAutocompleteEditTests { + private readonly ViewPort viewPort; + + public ViewPortAutocompleteEditTests() { + var model = new PokemonModel(new byte[0x200]); + viewPort = new ViewPort("name.txt", model) { Height = 0x10, Width = 0x10 }; + + viewPort.SelectionStart = new Point(0, 8); + viewPort.Edit("^label "); + + viewPort.SelectionStart = new Point(4, 8); + viewPort.Edit("^labwork "); + + viewPort.SelectionStart = new Point(8, 8); + viewPort.Edit("^othertext "); + + viewPort.SelectionStart = new Point(12, 8); + viewPort.Edit("^sometext "); + + viewPort.SelectionStart = new Point(); + } + + [SkippableFact] + public void UnderEditLoosePointerGetsAutoComplete() { + Skip.If(true); + viewPort.Edit("(viewPort[0, 0].Format); + } + + [SkippableFact] + public void AutocompleteWorksForEnums() { + Skip.If(true); + throw new NotImplementedException(); + } + } +} diff --git a/src/HexManiac.WPF/Windows/MainWindow.xaml b/src/HexManiac.WPF/Windows/MainWindow.xaml index 4879c628..956f45a7 100644 --- a/src/HexManiac.WPF/Windows/MainWindow.xaml +++ b/src/HexManiac.WPF/Windows/MainWindow.xaml @@ -227,7 +227,8 @@ - + +