HexManiacAdvance/src/HexManiac.Core/ViewModels/SearchResultsViewPort.cs
2021-06-21 22:20:12 -05:00

319 lines
13 KiB
C#

using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Tools;
using HavenSoft.HexManiac.Core.ViewModels.Visitors;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows.Input;
namespace HavenSoft.HexManiac.Core.ViewModels {
public class SearchResultsViewPort : ViewModelCore, IViewPort {
private readonly StubCommand scroll, close;
private readonly List<IChildViewPort> children = new List<IChildViewPort>();
private readonly Dictionary<IViewPort, int> firstChildToUseParent = new Dictionary<IViewPort, int>();
private readonly List<(int start, int end)> childrenSelection = new List<(int, int)>();
private int width, height, scrollValue, maxScrollValue;
public int ChildViewCount => children.Count;
public int ResultCount => children.Sum(child => (child is CompositeChildViewPort composite) ? composite.Count : 1);
#region Implementing IViewPort
public HexElement this[int x, int y] {
get {
var (childIndex, line) = GetChildLine(y);
if (line < 0 && x == 3 && childIndex < children.Count && firstChildToUseParent[children[childIndex].Parent] == childIndex) {
return new HexElement(0, false, new UnderEdit(null, "Results from " + children[childIndex].FileName, width));
}
if (line < 0 || childIndex >= children.Count) return HexElement.Undefined;
return children[childIndex][x, line];
}
}
public int Width {
get => width;
set {
if (TryUpdate(ref width, value)) NotifyCollectionChanged();
}
}
public int Height {
get => height;
set {
if (TryUpdate(ref height, value)) NotifyCollectionChanged();
}
}
public int MinimumScroll => 0;
public event EventHandler PreviewScrollChanged;
public int ScrollValue {
get => scrollValue;
set {
PreviewScrollChanged?.Invoke(this, EventArgs.Empty);
value = value.LimitToRange(0, MaximumScroll);
if (TryUpdate(ref scrollValue, value)) NotifyCollectionChanged();
}
}
public int MaximumScroll { get => maxScrollValue; private set => TryUpdate(ref maxScrollValue, value); }
public ObservableCollection<string> Headers { get; } = new ObservableCollection<string>();
public ICommand Scroll => scroll;
public int DataOffset {
get {
var (childIndex, line) = GetChildLine(0);
var child = children[childIndex];
return child.Width * line + child.DataOffset;
}
}
public string Name { get; }
public string FullFileName { get; }
public string FileName => string.Empty;
public ICommand Save { get; } = new StubCommand();
public ICommand SaveAs { get; } = new StubCommand();
public ICommand ExportBackup => null;
public ICommand Undo { get; } = new StubCommand();
public ICommand Redo { get; } = new StubCommand();
public ICommand Copy { get; } = new StubCommand();
public ICommand DeepCopy { get; } = new StubCommand();
public ICommand Diff => null;
public ICommand DiffLeft => null;
public ICommand DiffRight => null;
public ICommand Clear { get; } = new StubCommand();
public ICommand SelectAll { get; } = new StubCommand();
public ICommand Goto => null;
public ICommand ResetAlignment { get; } = new StubCommand();
public ICommand Back { get; } = new StubCommand();
public ICommand Forward { get; } = new StubCommand();
public ICommand Close => close;
public bool UpdateInProgress => false;
public double Progress => 0;
public IDataModel Model => null;
public bool HasTools => false;
public bool AutoAdjustDataWidth { get; set; }
public bool StretchData { get; set; }
public bool AllowMultipleElementsPerLine { get; set; }
public ChangeHistory<ModelDelta> ChangeHistory { get; }
public IToolTrayViewModel Tools { get; } = new SearchResultsTools();
public string SelectedAddress => string.Empty;
public string SelectedBytes => string.Empty;
public string AnchorText { get; set; }
public bool AnchorTextVisible => false;
public ObservableCollection<HeaderRow> ColumnHeaders { get; }
#pragma warning disable 0067 // it's ok if events are never used
public event EventHandler<string> OnError;
public event EventHandler<string> OnMessage;
public event EventHandler ClearMessage;
public event EventHandler Closed;
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event EventHandler<ITabContent> RequestTabChange;
public event EventHandler<Action> RequestDelayedWork;
public event EventHandler RequestMenuClose;
public event EventHandler<Direction> RequestDiff;
public event EventHandler<CanDiffEventArgs> RequestCanDiff;
#pragma warning restore 0067
#endregion
public SearchResultsViewPort(string searchTerm) {
FullFileName = $"Results for {searchTerm}";
Name = searchTerm.Length > 24 ? searchTerm.Substring(0, 23) + "…" : searchTerm;
width = 4;
height = 4;
scroll = new StubCommand {
CanExecute = arg => (Direction)arg != Direction.Left && (Direction)arg != Direction.Right,
Execute = arg => {
var direction = (Direction)arg;
if (direction == Direction.Up) ScrollValue--;
if (direction == Direction.Down) ScrollValue++;
},
};
close = new StubCommand {
CanExecute = arg => true,
Execute = arg => Closed?.Invoke(this, EventArgs.Empty),
};
}
public void Add(IChildViewPort child, int start, int end) {
if (children.Count > 0) {
var previousHeight = children.Last().Height;
if (CompositeChildViewPort.TryCombine(children.Last(), child, out var combo)) {
children[children.Count - 1] = combo;
maxScrollValue += combo.Height - previousHeight;
NotifyCollectionChanged();
return;
}
}
children.Add(child);
childrenSelection.Add((start, end));
maxScrollValue += child.Height;
width = Math.Max(width, child.Width);
if (children.Count > 1) maxScrollValue++;
if (!firstChildToUseParent.ContainsKey(child.Parent)) {
firstChildToUseParent.Add(child.Parent, children.Count - 1);
}
NotifyCollectionChanged();
}
public IChildViewPort CreateChildView(int startAddress, int endAddress) => throw new NotImplementedException();
// if asked to search the search results... just don't
public IReadOnlyList<(int, int)> Find(string search, bool matchExactCase = false) => new (int, int)[0];
public bool UseCustomHeaders {
get => children.FirstOrDefault()?.UseCustomHeaders ?? false;
set {
children.ForEach(child => child.UseCustomHeaders = value);
UpdateHeaders();
}
}
public bool IsSelected(Point point) {
var (x, y) = (point.X, point.Y);
if (y < 0 || y > height || x < 0 || x > width) return false;
var (childIndex, line) = GetChildLine(y);
if (line == -1 || childIndex >= children.Count) return false;
return children[childIndex].IsSelected(new Point(x, line));
}
public bool IsTable(Point point) => false;
public void FollowLink(int x, int y) {
if (y < 0 || y > height || x < 0 || x > width) return;
var (childIndex, line) = GetChildLine(y);
if (line == -1 || childIndex >= children.Count) return;
var child = children[childIndex];
var parent = child.Parent;
if (child.Model.GetNextRun(child.DataOffset) is ArrayRun array) {
parent.Goto.Execute(child.DataOffset.ToString("X6"));
parent.ScrollValue += line - y;
// heuristic: if the parent height matches the search results height, then the parent
// probably doesn't have labels yet but is about to get them. We don't know how big the
// labels will be, but they will probably push all the data down quite a bit.
// compensate by scrolling slightly
if (parent.Height == Height) parent.ScrollValue += 3;
} else {
var dataOffset = Math.Max(0, child.DataOffset - (y - line) * child.Width);
parent.Goto.Execute(dataOffset.ToString("X6"));
}
if (parent is ViewPort viewPort) {
SelectRange(viewPort, childrenSelection[childIndex]);
}
RequestTabChange?.Invoke(this, parent);
}
public static void SelectRange(ViewPort viewPort, (int start, int end) range) {
viewPort.SelectionStart = viewPort.ConvertAddressToViewPoint(range.start);
viewPort.SelectionEnd = viewPort.ConvertAddressToViewPoint(range.end);
}
public void ExpandSelection(int x, int y) => FollowLink(x, y);
public void ConsiderReload(IFileSystem fileSystem) { }
public void FindAllSources(int x, int y) {
if (y < 0 || y > height || x < 0 || x > width) return;
var (childIndex, line) = GetChildLine(y);
if (line == -1 || childIndex >= children.Count) return;
children[childIndex].FindAllSources(x, y);
}
public void ValidateMatchedWords() { }
public IReadOnlyList<IContextItem> GetContextMenuItems(Point selectionPoint) {
return new[] { new ContextItem("Open in Main Tab", arg => {
FollowLink(selectionPoint.X, selectionPoint.Y);
RequestMenuClose?.Invoke(this, EventArgs.Empty);
}) { ShortcutText = "Ctrl+Click" } };
}
public void Refresh() => children.ForEach(child => child.Refresh());
public bool TryImport(LoadedFile file, IFileSystem fileSystem) => false;
private void NotifyCollectionChanged() {
if (children.Count == 0) return;
UpdateHeaders();
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private void UpdateHeaders() {
Headers.Clear();
for (int i = 0; i < height; i++) {
var (childIndex, line) = GetChildLine(i);
if (line == -1 || childIndex >= children.Count) { // blank line between results / after results
Headers.Add(string.Empty);
} else {
Headers.Add(children[childIndex].Headers[line]);
}
}
}
/// <param name="y">0 is the first line in the view</param>
/// <returns>
/// Which child (and which line of that child) should be used to represent the y-line in the view,
/// based on scrolling in the overall search results and within each individual result.
/// </returns>
private (int childIndex, int childLineNumber) GetChildLine(int y) {
int line = y + scrollValue - 1; // 0 is the first line in the data. Include one empty line at the top for the file name
int childIndex = 0;
while (childIndex < children.Count && children[childIndex].Height <= line) {
line -= children[childIndex].Height + 1; childIndex++;
}
return (childIndex, line);
}
}
public class SearchResultsTools : IToolTrayViewModel {
public ICommand HideCommand { get; } = new StubCommand();
public ICommand StringToolCommand { get; } = new StubCommand();
public ICommand TableToolCommand { get; } = new StubCommand();
public ICommand SpriteToolCommand { get; } = new StubCommand();
public ICommand CodeToolCommand { get; } = new StubCommand();
public PCSTool StringTool => null;
public TableTool TableTool => null;
public SpriteTool SpriteTool => null;
public CodeTool CodeTool => 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 EventHandler<string> OnMessage;
public event PropertyChangedEventHandler PropertyChanged;
#pragma warning restore 0067
public void Schedule(Action action) => action();
public void RefreshContent() { }
public IToolViewModel this[int index] => null;
public int SelectedIndex { get => -1; set { } }
public IToolViewModel SelectedTool { get => null; set { } }
public int Count => 0;
public IEnumerator<IToolViewModel> GetEnumerator() => new List<IToolViewModel>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => new List<IToolViewModel>().GetEnumerator();
}
}