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 children = new List(); private readonly Dictionary firstChildToUseParent = new Dictionary(); 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 Headers { get; } = new ObservableCollection(); 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 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 ColumnHeaders { get; } #pragma warning disable 0067 // it's ok if events are never used public event EventHandler OnError; public event EventHandler OnMessage; public event EventHandler ClearMessage; public event EventHandler Closed; public event NotifyCollectionChangedEventHandler CollectionChanged; public event EventHandler RequestTabChange; public event EventHandler RequestDelayedWork; public event EventHandler RequestMenuClose; public event EventHandler RequestDiff; public event EventHandler 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 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]); } } } /// 0 is the first line in the view /// /// 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. /// 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 OnError; public event EventHandler 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 GetEnumerator() => new List().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => new List().GetEnumerator(); } }