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
This commit is contained in:
Benjamin Popp 2019-04-29 16:32:14 -05:00
parent 32020c62da
commit ebc9d1f33d
16 changed files with 164 additions and 37 deletions

View File

@ -53,6 +53,7 @@
<Compile Include="Models\Runs\ArrayRunElementSegment.cs" />
<Compile Include="Models\Runs\AsciiRun.cs" />
<Compile Include="Models\Runs\HeaderRow.cs" />
<Compile Include="ViewModels\AutoCompleteSelectionItem.cs" />
<Compile Include="ViewModels\Theme.cs" />
<Compile Include="ViewModels\Tools\IArrayElementViewModel.cs" />
<Compile Include="Models\Runs\IFormattedRun.cs" />

View File

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

View File

@ -8,6 +8,8 @@ namespace HavenSoft.HexManiac.Core.Models {
public IReadOnlyList<StoredAnchor> NamedAnchors { get; }
public IReadOnlyList<StoredUnmappedPointers> UnmappedPointers { get; }
public bool IsEmpty => NamedAnchors.Count == 0 && UnmappedPointers.Count == 0;
public StoredMetadata(IReadOnlyList<StoredAnchor> anchors, IReadOnlyList<StoredUnmappedPointers> unmappedPointers) {
NamedAnchors = anchors;
UnmappedPointers = unmappedPointers;

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace HavenSoft.HexManiac.Core.ViewModels {
public class AutoCompleteSelectionItem : IEquatable<AutoCompleteSelectionItem>, 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<AutoCompleteSelectionItem> Generate(IEnumerable<string> options, int selectionIndex) {
var list = new List<AutoCompleteSelectionItem>();
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;
}
}
}

View File

@ -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<AutoCompleteSelectionItem> AutocompleteOptions { get; }
public UnderEdit(IDataFormat original, string text, int editWidth = 1, IReadOnlyList<AutoCompleteSelectionItem> 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);
}

View File

@ -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<AutoCompleteSelectionItem> CreateAutoCompleteOptions(IEnumerable<string> options, int length) {
if (completionIndex >= length) {
completionIndex = length - 1;
NotifyPropertyChanged(nameof(CompletionIndex));
}
var list = new List<AutoCompleteSelectionItem>(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);
}
}

View File

@ -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<string> OnError;
public event PropertyChangedEventHandler PropertyChanged;
#pragma warning restore 0067
public void Schedule(Action action) => action();
public void RefreshContent() { }

View File

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

View File

@ -59,7 +59,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
public event EventHandler<IFormattedRun> 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<ModelDelta> history, IToolTrayViewModel toolTray) {
this.model = model;

View File

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

View File

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

View File

@ -9,7 +9,9 @@ using Xunit;
namespace HavenSoft.HexManiac.Tests {
public class FakeChangeToken : List<int>, 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<int> data) : base(data) { }

View File

@ -80,6 +80,7 @@
<Compile Include="NavigationTests.cs" />
<Compile Include="StringModelTests.cs" />
<Compile Include="ToolTests.cs" />
<Compile Include="ViewPortAutocompleteEditTests.cs" />
<Compile Include="ViewPortCursorTests.cs" />
<Compile Include="ViewPortEditTests.cs" />
<Compile Include="ViewPortSaveTests.cs" />

View File

@ -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("<labe");
var format = (UnderEdit)viewPort[0, 0].Format;
Assert.Single(format.AutocompleteOptions);
format = (UnderEdit)viewPort[1, 0].Format;
Assert.Null(format.AutocompleteOptions);
}
[SkippableFact]
public void BackspaceWidensAutocompleteResults() {
Skip.If(true);
viewPort.Edit("<labe");
viewPort.Edit(ConsoleKey.Backspace);
var format = (UnderEdit)viewPort[0, 0].Format;
Assert.Equal(2, format.AutocompleteOptions.Count);
}
[SkippableFact]
public void UpDownDuringAutoCompleteSelectsResults() {
Skip.If(true);
viewPort.Edit("<lab");
viewPort.MoveSelectionStart.Execute(Direction.Down);
var format = (UnderEdit)viewPort[0, 0].Format;
Assert.True(format.AutocompleteOptions[0].IsSelected);
}
[SkippableFact]
public void EmptyAutoCompleteArrowsActNormally() {
Skip.If(true);
viewPort.Edit("<xyz");
viewPort.MoveSelectionStart.Execute(Direction.Down);
Assert.IsNotType<UnderEdit>(viewPort[0, 0].Format);
}
[SkippableFact]
public void AutocompleteWorksForEnums() {
Skip.If(true);
throw new NotImplementedException();
}
}
}

View File

@ -227,7 +227,8 @@
</Button>
</Grid>
</DockPanel>
<Grid>
<Border DockPanel.Dock="Top" Height="1" Background="{DynamicResource Backlight}"/>
<Grid Background="{DynamicResource Background}">
<hsg3hv:StartScreen>
<hsg3hv:StartScreen.Style>
<Style TargetType="FrameworkElement">

View File

@ -9,7 +9,7 @@
</Grid.ColumnDefinitions>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Text="White:" Foreground="{Binding DarkColor}"/>
<TextBlock Text="Light Background / Text:" Foreground="{Binding DarkColor}"/>
<TextBox Foreground="{Binding DarkColor}" Background="{Binding HighlightLight}" Text="{Binding LightColor, UpdateSourceTrigger=PropertyChanged}" Width="150" Margin="5,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10">
@ -23,7 +23,7 @@
</StackPanel>
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Top" Grid.Column="1">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Text="Black:" Foreground="{Binding LightColor}"/>
<TextBlock Text="Dark Background / Text:" Foreground="{Binding LightColor}"/>
<TextBox Foreground="{Binding LightColor}" Background="{Binding HighlightDark}" Text="{Binding DarkColor, UpdateSourceTrigger=PropertyChanged}" Width="150" Margin="5,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10">