mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-05-21 21:00:55 -05:00
329 lines
13 KiB
C#
329 lines
13 KiB
C#
using HavenSoft.HexManiac.Core.Models;
|
|
using HavenSoft.HexManiac.Core.Models.Runs;
|
|
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
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(),
|
|
gotoCommand = new StubCommand(),
|
|
forward = new StubCommand(),
|
|
backward = new StubCommand();
|
|
|
|
// these back/forward stacks are not encapsulated in a history object because we want to be able to change a remembered address each time we visit it.
|
|
// if we navigate back, then scroll, then navigate forward, we want to remember the scroll if we go back again.
|
|
private readonly Stack<int> backStack = new Stack<int>(), forwardStack = new Stack<int>();
|
|
|
|
private int preferredWidth = DefaultPreferredWidth, maxWidth = 4;
|
|
|
|
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;
|
|
set {
|
|
var index = Scroll.ViewPointToDataIndex(value);
|
|
value = Scroll.DataIndexToViewPoint(index.LimitToRange(0, Scroll.DataLength));
|
|
|
|
if (selectionStart.Equals(value)) return;
|
|
|
|
if (!Scroll.ScrollToPoint(ref value)) {
|
|
PreviewSelectionStartChanged?.Invoke(this, getSpan(rawSelectionStart).start);
|
|
}
|
|
|
|
rawSelectionStart = value;
|
|
rawSelectionEnd = value;
|
|
var (start, end) = getSpan(rawSelectionStart);
|
|
TryUpdate(ref selectionStart, start);
|
|
TryUpdate(ref selectionEnd, end, nameof(SelectionEnd));
|
|
}
|
|
}
|
|
|
|
public Point SelectionEnd {
|
|
get => selectionEnd;
|
|
set {
|
|
var index = Scroll.ViewPointToDataIndex(value);
|
|
value = Scroll.DataIndexToViewPoint(index.LimitToRange(0, Scroll.DataLength));
|
|
|
|
if (selectionEnd.Equals(value)) return;
|
|
|
|
Scroll.ScrollToPoint(ref 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
public int PreferredWidth {
|
|
get => preferredWidth;
|
|
set {
|
|
if (TryUpdate(ref preferredWidth, value)) ChangeWidth(maxWidth);
|
|
}
|
|
}
|
|
|
|
public ICommand MoveSelectionStart => moveSelectionStart;
|
|
public ICommand MoveSelectionEnd => moveSelectionEnd;
|
|
public ICommand Goto => gotoCommand;
|
|
public ICommand Forward => forward;
|
|
public ICommand Back => backward;
|
|
|
|
public ScrollRegion Scroll { get; }
|
|
|
|
public event EventHandler<string> OnError;
|
|
|
|
/// <summary>
|
|
/// The owner may have something special going on with the selected point.
|
|
/// Warn the owner before the selection changes, in case they need to do cleanup.
|
|
/// </summary>
|
|
public event EventHandler<Point> PreviewSelectionStartChanged;
|
|
|
|
public Selection(ScrollRegion scrollRegion, IDataModel model, GetSelectionSpan getSpan = null) {
|
|
this.model = model;
|
|
this.getSpan = getSpan ?? GetDefaultSelectionSpan;
|
|
Scroll = scrollRegion;
|
|
Scroll.ScrollChanged += (sender, e) => ShiftSelectionFromScroll(e);
|
|
|
|
moveSelectionStart.CanExecute = args => true;
|
|
moveSelectionStart.Execute = args => MoveSelectionStartExecuted((Direction)args);
|
|
moveSelectionEnd.CanExecute = args => true;
|
|
moveSelectionEnd.Execute = args => MoveSelectionEndExecuted((Direction)args);
|
|
|
|
gotoCommand = new StubCommand {
|
|
CanExecute = args => true,
|
|
Execute = args => {
|
|
var address = args.ToString().Trim();
|
|
if (address.StartsWith(PointerRun.PointerStart.ToString())) address = address.Substring(1);
|
|
if (address.EndsWith(PointerRun.PointerEnd.ToString())) address = address.Substring(0, address.Length - 1);
|
|
var anchor = this.model.GetAddressFromAnchor(new ModelDelta(), -1, address);
|
|
using (ModelCacheScope.CreateScope(this.model)) {
|
|
if (anchor != Pointer.NULL) {
|
|
GotoAddress(anchor);
|
|
} else if (int.TryParse(address, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out int result)) {
|
|
if (result >= BaseModel.PointerOffset) result -= BaseModel.PointerOffset;
|
|
GotoAddress(result);
|
|
} else {
|
|
OnError?.Invoke(this, $"Unable to goto address '{address}'");
|
|
}
|
|
}
|
|
},
|
|
};
|
|
backward = new StubCommand {
|
|
CanExecute = args => backStack.Count > 0,
|
|
Execute = args => {
|
|
if (backStack.Count == 0) return;
|
|
forwardStack.Push(Scroll.DataIndex);
|
|
if (forwardStack.Count == 1) forward.CanExecuteChanged.Invoke(forward, EventArgs.Empty);
|
|
GotoAddressHelper(backStack.Pop());
|
|
if (backStack.Count == 0) backward.CanExecuteChanged.Invoke(backward, EventArgs.Empty);
|
|
},
|
|
};
|
|
forward = new StubCommand {
|
|
CanExecute = args => forwardStack.Count > 0,
|
|
Execute = args => {
|
|
if (forwardStack.Count == 0) return;
|
|
backStack.Push(Scroll.DataIndex);
|
|
if (backStack.Count == 1) backward.CanExecuteChanged.Invoke(backward, EventArgs.Empty);
|
|
GotoAddressHelper(forwardStack.Pop());
|
|
if (forwardStack.Count == 0) forward.CanExecuteChanged.Invoke(forward, EventArgs.Empty);
|
|
},
|
|
};
|
|
}
|
|
|
|
public bool IsSelected(Point point) {
|
|
if (point.X < 0 || point.X >= Scroll.Width) return false;
|
|
|
|
var selectionStart = Scroll.ViewPointToDataIndex(SelectionStart);
|
|
var selectionEnd = Scroll.ViewPointToDataIndex(SelectionEnd);
|
|
var middle = Scroll.ViewPointToDataIndex(point);
|
|
|
|
var leftEdge = Math.Min(selectionStart, selectionEnd);
|
|
var rightEdge = Math.Max(selectionStart, selectionEnd);
|
|
|
|
return leftEdge <= middle && middle <= rightEdge;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Changing the scrollregion's width visibly moves the selection.
|
|
/// But if we updated the selection using SelectionStart and SelectionEnd, it would auto-scroll.
|
|
/// </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);
|
|
|
|
rawSelectionStart = Scroll.DataIndexToViewPoint(rawStart);
|
|
rawSelectionEnd = Scroll.DataIndexToViewPoint(rawEnd);
|
|
selectionStart = Scroll.DataIndexToViewPoint(start);
|
|
selectionEnd = Scroll.DataIndexToViewPoint(end);
|
|
}
|
|
|
|
public void GotoAddress(int address) {
|
|
if (address > Scroll.DataLength || address < 0) {
|
|
OnError?.Invoke(this, $"Address {address:X2} is not within the size of the data.");
|
|
return;
|
|
}
|
|
|
|
backStack.Push(Scroll.DataIndex);
|
|
if (backStack.Count == 1) backward.CanExecuteChanged.Invoke(backward, EventArgs.Empty);
|
|
if (forwardStack.Count > 0) {
|
|
forwardStack.Clear();
|
|
forward.CanExecuteChanged.Invoke(forward, EventArgs.Empty);
|
|
}
|
|
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;
|
|
int preferredWidth;
|
|
if (destinationIsArray) {
|
|
preferredWidth = destinationRun.ElementLength;
|
|
} else {
|
|
preferredWidth = DefaultPreferredWidth;
|
|
}
|
|
|
|
using (ModelCacheScope.CreateScope(model)) {
|
|
var startAddress = address;
|
|
if (!destinationIsArray && preferredWidth > 1) address -= address % preferredWidth;
|
|
|
|
// first, change the selection and scroll to select the actual requested address
|
|
SelectionStart = Scroll.DataIndexToViewPoint(startAddress);
|
|
Scroll.ScrollValue += selectionStart.Y;
|
|
|
|
// then, scroll left/right as needed to align everything
|
|
while (Scroll.DataIndex < address) Scroll.Scroll.Execute(Direction.Right);
|
|
while (Scroll.DataIndex > address) Scroll.Scroll.Execute(Direction.Left);
|
|
|
|
PreferredWidth = preferredWidth;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When the scrolling changes, the selection has to move as well.
|
|
/// This is because the selection is in terms of the viewPort, not the overall data.
|
|
/// 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);
|
|
}
|
|
|
|
private void MoveSelectionStartExecuted(Direction direction) {
|
|
Point dif;
|
|
if (direction == Direction.PageUp) {
|
|
dif = new Point(0, -Scroll.Height);
|
|
} else if (direction == Direction.PageDown) {
|
|
dif = new Point(0, Scroll.Height);
|
|
} else {
|
|
dif = ScrollRegion.DirectionToDif[direction];
|
|
}
|
|
|
|
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) {
|
|
Point dif;
|
|
if (direction == Direction.PageUp) {
|
|
dif = new Point(0, -Scroll.Height);
|
|
} else if (direction == Direction.PageDown) {
|
|
dif = new Point(0, Scroll.Height);
|
|
} else {
|
|
dif = ScrollRegion.DirectionToDif[direction];
|
|
}
|
|
|
|
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) {
|
|
if (preferredWidth == -1 || preferredWidth == width) return width;
|
|
if (preferredWidth < width) {
|
|
int multiple = 2;
|
|
while (preferredWidth * multiple <= width) multiple++;
|
|
return preferredWidth * (multiple - 1);
|
|
}
|
|
var divisors = GetDivisors(preferredWidth).Reverse();
|
|
var newWidth = divisors.FirstOrDefault();
|
|
if (newWidth < 4) return preferredWidth;
|
|
return newWidth;
|
|
}
|
|
|
|
private static IEnumerable<int> GetDivisors(int number) {
|
|
// only actually allow for divisors if the preferred width is 0x10
|
|
if (number == 16) {
|
|
for (int i = 1; i <= number / 2; i++) {
|
|
if (number % i == 0) yield return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|