update: make it impossible to select a single cell of a pointer/int/enum.

all the cells come together as a single package.
this clears up some confusion as to which byte you're editing... hopefully.
This commit is contained in:
Benjamin Popp
2019-04-26 07:10:59 -05:00
parent 81e8f8bd1b
commit ecb2ceb2fb
4 changed files with 229 additions and 16 deletions

View File

@@ -8,11 +8,13 @@ using System.Linq;
using System.Windows.Input;
namespace HavenSoft.HexManiac.Core.ViewModels {
public delegate (Point start, Point end) GetSelectionSpan(Point p);
public class Selection : ViewModelCore {
private const int DefaultPreferredWidth = 0x10;
private readonly IDataModel model;
private readonly GetSelectionSpan getSpan;
private readonly StubCommand
moveSelectionStart = new StubCommand(),
moveSelectionEnd = new StubCommand(),
@@ -26,7 +28,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
private int preferredWidth = DefaultPreferredWidth, maxWidth = 4;
private Point selectionStart, selectionEnd;
private Point rawSelectionStart; // the actual click point
private Point selectionStart; // the calculated selection start, which may differ depending on the SelectionSpan
private Point rawSelectionEnd; // the actual release point
private Point selectionEnd; // the calculated selection end, which may differ depending on the SelectionSpan
public Point SelectionStart {
get => selectionStart;
@@ -37,12 +42,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
if (selectionStart.Equals(value)) return;
if (!Scroll.ScrollToPoint(ref value)) {
PreviewSelectionStartChanged?.Invoke(this, selectionStart);
PreviewSelectionStartChanged?.Invoke(this, rawSelectionStart);
}
if (TryUpdate(ref selectionStart, value)) {
SelectionEnd = selectionStart;
}
rawSelectionStart = value;
rawSelectionEnd = value;
var (start, end) = getSpan(rawSelectionStart);
TryUpdate(ref selectionStart, start);
TryUpdate(ref selectionEnd, end, nameof(SelectionEnd));
}
}
@@ -52,8 +59,35 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
var index = Scroll.ViewPointToDataIndex(value);
value = Scroll.DataIndexToViewPoint(index.LimitToRange(0, Scroll.DataLength));
if (selectionEnd.Equals(value)) return;
Scroll.ScrollToPoint(ref value);
TryUpdate(ref selectionEnd, value);
rawSelectionEnd = value;
var startIndex = Scroll.ViewPointToDataIndex(rawSelectionStart);
var endIndex = Scroll.ViewPointToDataIndex(rawSelectionEnd);
// case 1: start/end are the same
if (startIndex == endIndex) {
var (start, end) = getSpan(rawSelectionStart);
TryUpdate(ref selectionStart, start, nameof(SelectionStart));
TryUpdate(ref selectionEnd, end);
return;
}
// case 2: start < end
if (startIndex < endIndex) {
TryUpdate(ref selectionStart, getSpan(rawSelectionStart).start, nameof(SelectionStart));
TryUpdate(ref selectionEnd, getSpan(rawSelectionEnd).end);
return;
}
// case 3: start > end
if (startIndex > endIndex) {
TryUpdate(ref selectionEnd, getSpan(rawSelectionEnd).start);
TryUpdate(ref selectionStart, getSpan(rawSelectionStart).end, nameof(SelectionStart));
return;
}
}
}
@@ -80,8 +114,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
/// </summary>
public event EventHandler<Point> PreviewSelectionStartChanged;
public Selection(ScrollRegion scrollRegion, IDataModel model) {
public Selection(ScrollRegion scrollRegion, IDataModel model, GetSelectionSpan getSpan = null) {
this.model = model;
this.getSpan = getSpan ?? GetDefaultSelectionSpan;
Scroll = scrollRegion;
Scroll.ScrollChanged += (sender, e) => ShiftSelectionFromScroll(e);
@@ -152,13 +187,17 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
/// </summary>
public void ChangeWidth(int newWidth) {
maxWidth = newWidth;
var rawStart = Scroll.ViewPointToDataIndex(rawSelectionStart);
var rawEnd = Scroll.ViewPointToDataIndex(rawSelectionEnd);
var start = Scroll.ViewPointToDataIndex(selectionStart);
var end = Scroll.ViewPointToDataIndex(selectionEnd);
Scroll.Width = CoerceWidth(newWidth);
TryUpdate(ref selectionStart, Scroll.DataIndexToViewPoint(start));
TryUpdate(ref selectionEnd, Scroll.DataIndexToViewPoint(end));
rawSelectionStart = Scroll.DataIndexToViewPoint(rawStart);
rawSelectionEnd = Scroll.DataIndexToViewPoint(rawEnd);
selectionStart = Scroll.DataIndexToViewPoint(start);
selectionEnd = Scroll.DataIndexToViewPoint(end);
}
public void GotoAddress(int address) {
@@ -171,6 +210,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
GotoAddressHelper(address);
}
private static (Point start, Point end) GetDefaultSelectionSpan(Point p) => (p, p);
private void GotoAddressHelper(int address) {
var destinationRun = model.GetNextRun(address) as ArrayRun;
var destinationIsArray = destinationRun != null;
@@ -201,16 +242,23 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
/// Nothing in this method notifies because any amount of scrolling means we already need a complete redraw.
/// </summary>
private void ShiftSelectionFromScroll(int distance) {
var rawStart = Scroll.ViewPointToDataIndex(rawSelectionStart);
var rawEnd = Scroll.ViewPointToDataIndex(rawSelectionEnd);
var start = Scroll.ViewPointToDataIndex(selectionStart);
var end = Scroll.ViewPointToDataIndex(selectionEnd);
rawStart -= distance;
rawEnd -= distance;
start -= distance;
end -= distance;
rawSelectionStart = Scroll.DataIndexToViewPoint(rawStart);
rawSelectionEnd = Scroll.DataIndexToViewPoint(rawEnd);
selectionStart = Scroll.DataIndexToViewPoint(start);
selectionEnd = Scroll.DataIndexToViewPoint(end);
}
// TODO update these two methods to work with rawSelectionStart / rawSelectionEnd
private void MoveSelectionStartExecuted(Direction direction) {
Point dif;
if (direction == Direction.PageUp) {
@@ -220,7 +268,15 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
} else {
dif = ScrollRegion.DirectionToDif[direction];
}
SelectionStart = SelectionEnd + dif;
var (start, end) = getSpan(rawSelectionEnd);
if (dif.X < 0 || dif.Y < 0) {
// start from the _front_ of selectionEnd
SelectionStart = start + dif;
} else {
// start from the _back_ of selectionEnd
SelectionStart = end + dif;
}
}
private void MoveSelectionEndExecuted(Direction direction) {
@@ -232,7 +288,15 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
} else {
dif = ScrollRegion.DirectionToDif[direction];
}
SelectionEnd += dif;
var (start, end) = getSpan(rawSelectionEnd);
if (dif.X < 0 || dif.Y < 0) {
// start from the _front_ of selectionEnd
SelectionEnd = start + dif;
} else {
// start from the _back_ of selectionEnd
SelectionEnd = end + dif;
}
}
private int CoerceWidth(int width) {

View File

@@ -353,7 +353,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
scroll = new ScrollRegion(model.TryGetUsefulHeader) { DataLength = Model.Count };
scroll.PropertyChanged += ScrollPropertyChanged;
selection = new Selection(scroll, Model);
selection = new Selection(scroll, Model, GetSelectionSpan);
selection.PropertyChanged += SelectionPropertyChanged;
selection.PreviewSelectionStartChanged += ClearActiveEditBeforeSelectionChanges;
selection.OnError += (sender, e) => OnError?.Invoke(this, e);
@@ -505,6 +505,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
RefreshBackingData();
}
// TODO rethink backspace handling
// should it be changing just _before_ the selection,
// or should it be changing the _current_ selection?
run = Model.GetNextRun(index - 1);
if (run is PCSRun pcs) {
for (int i = index - 1; i < run.Start + run.Length; i++) history.CurrentChange.ChangeData(Model, i, 0xFF);
@@ -520,6 +524,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
RefreshBackingData();
SilentScroll(index - 1);
} else {
// TODO
throw new NotImplementedException();
}
} else if (run.Start <= index - 1 && run.Start + run.Length > index - 1) {
@@ -529,11 +534,17 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
element = currentView[SelectionStart.X, SelectionStart.Y];
element.Format.Visit(cellToText, element.Value);
var text = cellToText.Result;
var editLength = 1;
if (element.Format is Pointer pointer) editLength = 4;
if (element.Format is Integer integer) editLength = integer.Length;
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;
currentView[p.X, p.Y] = new HexElement(currentView[p.X, p.Y].Value, currentView[p.X, p.Y].Format.Edit(editString));
currentView[p.X, p.Y] = new HexElement(currentView[p.X, p.Y].Value, new UnderEdit(currentView[p.X, p.Y].Format, editString, editLength));
}
} else {
SelectionStart = scroll.DataIndexToViewPoint(index - 1);
element = currentView[SelectionStart.X, SelectionStart.Y];
@@ -978,6 +989,22 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
return false;
}
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);
if (run is PointerRun) return (scroll.DataIndexToViewPoint(run.Start), scroll.DataIndexToViewPoint(run.Start + run.Length - 1));
if (!(run is ArrayRun array)) return (p, p);
var offset = array.ConvertByteOffsetToArrayOffset(index);
if (array.ElementContent[offset.SegmentIndex].Type == ElementContentType.Pointer || array.ElementContent[offset.SegmentIndex].Type == ElementContentType.Integer) {
return (scroll.DataIndexToViewPoint(offset.SegmentStart), scroll.DataIndexToViewPoint(offset.SegmentStart + array.ElementContent[offset.SegmentIndex].Length - 1));
}
return (p, p);
}
private bool TryCoerceSelectionToStartOfElement(ref Point point, ref HexElement element) {
var format = element.Format;
var (position, length) = (-1, -1);

View File

@@ -308,7 +308,7 @@ namespace HavenSoft.HexManiac.Tests {
viewPort.Edit("01 02 03 04"); // 2x4 characters to clear
viewPort.Edit("<000020>"); // 8 characters to clear
viewPort.Edit("<000030>"); // 8 characters to clear
viewPort.SelectionStart = new Point(10, 1);
viewPort.SelectionStart = new Point(12, 1); // just after <000030>
for (int i = 0; i < 21; i++) viewPort.Edit(ConsoleKey.Backspace); // should clear both pointers (16) and 2 bytes (4)
viewPort.MoveSelectionStart.Execute(Direction.Up);
@@ -330,8 +330,9 @@ namespace HavenSoft.HexManiac.Tests {
viewPort.SelectionStart = new Point(0, 1);
viewPort.Edit("<000020>");
viewPort.Edit("<000030>");
viewPort.SelectionStart = new Point(2, 1);
viewPort.SelectionStart = new Point(2, 1); // note that this will select all four bytes of <000020>
viewPort.Clear.Execute(); // this should clear the data and formatting of the first pointer
viewPort.SelectionStart = new Point(2, 1); // this selects just the one byte
viewPort.Edit("<000040>"); // this should remove the second pointer
Assert.Equal(0xFF, viewPort[0, 1].Value);

View File

@@ -226,5 +226,126 @@ namespace HavenSoft.HexManiac.Tests {
Assert.True(viewPort.IsSelected(new Point(3, 0)));
Assert.False(viewPort.IsSelected(new Point(4, 0)));
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void SelectingAnyOfAPointerSelectsAllOfAPointer(int index) {
var data = new byte[0x200];
var model = new PokemonModel(data);
var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 };
viewPort.Edit("<000100>");
viewPort.SelectionStart = new Point(index, 0);
Assert.True(viewPort.IsSelected(new Point(0, 0)));
Assert.True(viewPort.IsSelected(new Point(1, 0)));
Assert.True(viewPort.IsSelected(new Point(2, 0)));
Assert.True(viewPort.IsSelected(new Point(3, 0)));
}
[Fact]
public void SelectLeftSelectsWholePointer() {
var data = new byte[0x200];
var model = new PokemonModel(data);
var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 };
viewPort.SelectionStart = new Point(4, 0);
viewPort.Edit("<000100>");
viewPort.SelectionStart = new Point(8, 0);
viewPort.MoveSelectionStart.Execute(Direction.Left);
Assert.True(viewPort.IsSelected(new Point(4, 0)));
Assert.True(viewPort.IsSelected(new Point(5, 0)));
Assert.True(viewPort.IsSelected(new Point(6, 0)));
Assert.True(viewPort.IsSelected(new Point(7, 0)));
}
[Fact]
public void SelectRightSelectsWholePointer() {
var data = new byte[0x200];
var model = new PokemonModel(data);
var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 };
viewPort.SelectionStart = new Point(4, 0);
viewPort.Edit("<000100>");
viewPort.SelectionStart = new Point(3, 0);
viewPort.MoveSelectionStart.Execute(Direction.Right);
Assert.True(viewPort.IsSelected(new Point(4, 0)));
Assert.True(viewPort.IsSelected(new Point(5, 0)));
Assert.True(viewPort.IsSelected(new Point(6, 0)));
Assert.True(viewPort.IsSelected(new Point(7, 0)));
}
[Fact]
public void SelectUpSelectsWholePointer() {
var data = new byte[0x200];
var model = new PokemonModel(data);
var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 };
viewPort.SelectionStart = new Point(4, 0);
viewPort.Edit("<000100>");
viewPort.SelectionStart = new Point(5, 1);
viewPort.MoveSelectionStart.Execute(Direction.Up);
Assert.True(viewPort.IsSelected(new Point(4, 0)));
Assert.True(viewPort.IsSelected(new Point(5, 0)));
Assert.True(viewPort.IsSelected(new Point(6, 0)));
Assert.True(viewPort.IsSelected(new Point(7, 0)));
}
[Fact]
public void SelectDownSelectsWholePointer() {
var data = new byte[0x200];
var model = new PokemonModel(data);
var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 };
viewPort.SelectionStart = new Point(4, 1);
viewPort.Edit("<000100>");
viewPort.SelectionStart = new Point(5, 0);
viewPort.MoveSelectionStart.Execute(Direction.Down);
Assert.True(viewPort.IsSelected(new Point(4, 1)));
Assert.True(viewPort.IsSelected(new Point(5, 1)));
Assert.True(viewPort.IsSelected(new Point(6, 1)));
Assert.True(viewPort.IsSelected(new Point(7, 1)));
}
[Fact]
public void HighlightLeftSelectsWholePointer() {
var data = new byte[0x200];
var model = new PokemonModel(data);
var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 };
viewPort.SelectionStart = new Point(4, 1);
viewPort.Edit("<000100>");
viewPort.SelectionStart = new Point(6, 1);
viewPort.SelectionEnd = new Point(2, 1);
Assert.True(viewPort.IsSelected(new Point(4, 1)));
Assert.True(viewPort.IsSelected(new Point(5, 1)));
Assert.True(viewPort.IsSelected(new Point(6, 1)));
Assert.True(viewPort.IsSelected(new Point(7, 1)));
}
[Fact]
public void HighlightRightSelectsWholePointer() {
var data = new byte[0x200];
var model = new PokemonModel(data);
var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 };
viewPort.SelectionStart = new Point(4, 1);
viewPort.Edit("<000100>");
viewPort.SelectionStart = new Point(6, 1);
viewPort.SelectionEnd = new Point(10, 1);
Assert.True(viewPort.IsSelected(new Point(4, 1)));
Assert.True(viewPort.IsSelected(new Point(5, 1)));
Assert.True(viewPort.IsSelected(new Point(6, 1)));
Assert.True(viewPort.IsSelected(new Point(7, 1)));
}
}
}