diff --git a/src/Gen3Hex.Core/ViewModels/IViewPort.cs b/src/Gen3Hex.Core/ViewModels/IViewPort.cs index 85cf64ca..86d6c84f 100644 --- a/src/Gen3Hex.Core/ViewModels/IViewPort.cs +++ b/src/Gen3Hex.Core/ViewModels/IViewPort.cs @@ -25,5 +25,6 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { IChildViewPort CreateChildView(int offset); void FollowLink(int x, int y); void ConsiderReload(IFileSystem fileSystem); + void FindAllSources(int x, int y); } } diff --git a/src/Gen3Hex.Core/ViewModels/SearchResultsViewPort.cs b/src/Gen3Hex.Core/ViewModels/SearchResultsViewPort.cs index 9ec40ff0..3dbac3a1 100644 --- a/src/Gen3Hex.Core/ViewModels/SearchResultsViewPort.cs +++ b/src/Gen3Hex.Core/ViewModels/SearchResultsViewPort.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Linq; using System.Windows.Input; namespace HavenSoft.Gen3Hex.Core.ViewModels { @@ -124,6 +125,14 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { 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); + } + private void NotifyCollectionChanged() { if (children.Count == 0) return; UpdateHeaders(); @@ -155,5 +164,6 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { } return (childIndex, line); } + } } diff --git a/src/Gen3Hex.Core/ViewModels/Selection.cs b/src/Gen3Hex.Core/ViewModels/Selection.cs index 113e1e63..81814333 100644 --- a/src/Gen3Hex.Core/ViewModels/Selection.cs +++ b/src/Gen3Hex.Core/ViewModels/Selection.cs @@ -66,17 +66,6 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { /// public event EventHandler PreviewSelectionStartChanged; - private void GotoAddress(int address) { - backStack.Push(scroll.DataIndex); - if (backStack.Count == 1) backward.CanExecuteChanged.Invoke(backward, EventArgs.Empty); - if (forwardStack.Count > 0) { - forward.CanExecuteChanged.Invoke(forward, EventArgs.Empty); - forwardStack.Clear(); - } - SelectionStart = scroll.DataIndexToViewPoint(address); - scroll.ScrollValue += selectionStart.Y; - } - public Selection(ScrollRegion scrollRegion, IModel model) { scroll = scrollRegion; scroll.ScrollChanged += (sender, e) => ShiftSelectionFromScroll(e); @@ -92,9 +81,9 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { var address = args.ToString(); var anchor = model.GetAddressFromAnchor(-1, address); if (anchor != Pointer.NULL) { - GotoAddress(anchor); + GotoAddressLine(anchor); } else if (int.TryParse(address, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out int result)) { - GotoAddress(result); + GotoAddressLine(result); } else { OnError?.Invoke(this, $"Unable to goto address '{address}'"); } @@ -151,6 +140,29 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { TryUpdate(ref selectionEnd, scroll.DataIndexToViewPoint(end)); } + public void GotoAddress(int address) { + 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); + } + SelectionStart = scroll.DataIndexToViewPoint(address); + scroll.ScrollValue += selectionStart.Y; + while (scroll.DataIndex < address) scroll.Scroll.Execute(Direction.Right); + } + + private void GotoAddressLine(int address) { + 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); + } + SelectionStart = scroll.DataIndexToViewPoint(address); + scroll.ScrollValue += selectionStart.Y; + } + /// /// 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. diff --git a/src/Gen3Hex.Core/ViewModels/ViewPort.cs b/src/Gen3Hex.Core/ViewModels/ViewPort.cs index b763e614..1603ebd8 100644 --- a/src/Gen3Hex.Core/ViewModels/ViewPort.cs +++ b/src/Gen3Hex.Core/ViewModels/ViewPort.cs @@ -378,7 +378,16 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { return child; } - public void FollowLink(int x, int y) { } + public void FollowLink(int x, int y) { + var format = currentView[x, y].Format; + if (format is Pointer pointer) { + if (pointer.Destination != Pointer.NULL) { + selection.GotoAddress(pointer.Destination); + } else { + OnError(this, $"Pointer destination {pointer.DestinationName} not found."); + } + } + } public void ConsiderReload(IFileSystem fileSystem) { if (!history.IsSaved) return; // don't overwrite local changes @@ -400,6 +409,18 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { } } + public virtual void FindAllSources(int x, int y) { + var anchor = currentView[x, y].Format as DataFormats.Anchor; + if (anchor == null) return; + var title = string.IsNullOrEmpty(anchor.Name) ? (y * Width + x + scroll.DataIndex).ToString("X6") : anchor.Name; + title = "Sources of " + title; + var newTab = new SearchResultsViewPort(title); + + foreach (var source in anchor.Sources) newTab.Add(CreateChildView(source)); + + RequestTabChange(this, newTab); + } + private void Edit(char input) { var point = GetEditPoint(); var element = currentView[point.X, point.Y]; diff --git a/src/Gen3Hex.Tests/ModelTests.cs b/src/Gen3Hex.Tests/ModelTests.cs index cb00a7f1..cfa4da2e 100644 --- a/src/Gen3Hex.Tests/ModelTests.cs +++ b/src/Gen3Hex.Tests/ModelTests.cs @@ -382,6 +382,36 @@ namespace HavenSoft.Gen3Hex.Tests { Assert.Single(results); } + [Fact] + public void CanUsePointerAsLink() { + var buffer = new byte[0x200]; + var model = new PointerModel(buffer); + var viewPort = new ViewPort(new LoadedFile("test.txt", buffer), model) { Width = 0x10, Height = 0x10 }; + + viewPort.SelectionStart = new Point(4, 1); + viewPort.Edit("<000120>"); + viewPort.FollowLink(4, 1); + + Assert.Equal("000120", viewPort.Headers[0]); + } + + [Fact] + public void FindAllSourcesWorks() { + var buffer = new byte[0x200]; + var model = new PointerModel(buffer); + var viewPort = new ViewPort(new LoadedFile("test.txt", buffer), model) { Width = 0x10, Height = 0x10 }; + var editor = new EditorViewModel(new StubFileSystem()); + editor.Add(viewPort); + + viewPort.SelectionStart = new Point(4, 1); + viewPort.Edit("<000120>"); + viewPort.FollowLink(4, 1); + + viewPort.FindAllSources(0, 0); + + Assert.Equal(1, editor.SelectedIndex); + } + // TODO putting a new anchor with the same name: delete any run that starts at that anchor, repoint everything from the old location to the new location // TODO undo/redo diff --git a/src/Gen3Hex.WPF/Controls/HexContent.cs b/src/Gen3Hex.WPF/Controls/HexContent.cs index 03060f27..b9410398 100644 --- a/src/Gen3Hex.WPF/Controls/HexContent.cs +++ b/src/Gen3Hex.WPF/Controls/HexContent.cs @@ -7,6 +7,8 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; @@ -115,8 +117,8 @@ namespace HavenSoft.Gen3Hex.WPF.Controls { return; } if (e.ChangedButton != MouseButton.Left) return; - Focus(); var p = ControlCoordinatesToModelCoordinates(e); + Focus(); if (e.ClickCount == 2) { ViewPort.FollowLink(p.X, p.Y); return; @@ -141,6 +143,11 @@ namespace HavenSoft.Gen3Hex.WPF.Controls { protected override void OnMouseUp(MouseButtonEventArgs e) { base.OnMouseUp(e); + if (e.ChangedButton == MouseButton.Right && e.LeftButton == MouseButtonState.Released && !IsMouseCaptured) { + var p = ControlCoordinatesToModelCoordinates(e); + if (ViewPort[p.X, p.Y].Format is Core.ViewModels.DataFormats.Anchor) ShowAnchorMenu(p); + return; + } if (!IsMouseCaptured) return; ReleaseMouseCapture(); } @@ -192,6 +199,47 @@ namespace HavenSoft.Gen3Hex.WPF.Controls { } } + private void ShowAnchorMenu(Core.Models.Point p) { + var anchor = (Core.ViewModels.DataFormats.Anchor)ViewPort[p.X, p.Y].Format; + + var panel = new StackPanel { Background = Solarized.Theme.Background }; + var menu = new Popup { Placement = PlacementMode.Mouse, Child = panel, StaysOpen = false }; + + if (anchor.Sources.Count > 1) { + panel.Children.Add(new Button { + Content = "Show All Sources in new tab" + }.SetEvent(ButtonBase.ClickEvent, (sender, e) => { + ViewPort.FindAllSources(p.X, p.Y); + menu.IsOpen = false; + })); + } + + if (anchor.Sources.Count < 5) { + for (int i = 0; i < anchor.Sources.Count; i++) { + var source = anchor.Sources[i].ToString("X2"); + panel.Children.Add(new Button { + Content = source, + }.SetEvent(ButtonBase.ClickEvent, (sender, e) => { + ViewPort.Goto.Execute(source); + menu.IsOpen = false; + })); + } + } else { + panel.Children.Add(new ScrollViewer { + MaxHeight = 120, + Content = new ListBox { + ItemsSource = anchor.Sources.Select(source => source.ToString("X2")).ToList(), + }.SetEvent(Selector.SelectionChangedEvent, (sender, e) => { + var source = anchor.Sources[((ListBox)sender).SelectedIndex].ToString("X2"); + ViewPort.Goto.Execute(source); + menu.IsOpen = false; + }), + }); + } + + menu.IsOpen = true; + } + private void UpdateViewPortSize() { ViewPort.Width = (int)(ActualWidth / CellWidth); ViewPort.Height = (int)(ActualHeight / CellHeight); @@ -202,4 +250,11 @@ namespace HavenSoft.Gen3Hex.WPF.Controls { return new Core.Models.Point((int)(point.X / CellWidth), (int)(point.Y / CellHeight)); } } + + public static class FrameworkElementExtensions { + public static T SetEvent(this T item, RoutedEvent routedEvent, RoutedEventHandler handler) where T : FrameworkElement { + item.AddHandler(routedEvent, handler); + return item; + } + } } diff --git a/src/Gen3Hex.WPF/Implementations/FormatDrawer.cs b/src/Gen3Hex.WPF/Implementations/FormatDrawer.cs index 58c81d9b..19d066e7 100644 --- a/src/Gen3Hex.WPF/Implementations/FormatDrawer.cs +++ b/src/Gen3Hex.WPF/Implementations/FormatDrawer.cs @@ -64,10 +64,10 @@ namespace HavenSoft.Gen3Hex.WPF.Implementations { context.DrawText(text, new Point(CellTextOffset.X - 10, CellTextOffset.Y)); } - private static readonly Geometry Triangle = Geometry.Parse("M1,0 L4,5 -2,5 Z"); + private static readonly Geometry Triangle = Geometry.Parse("M0,5 L3,0 6,5"); public void Visit(Anchor anchor, byte data) { anchor.OriginalFormat.Visit(this, data); - context.DrawGeometry(Solarized.Brushes.Blue, null, Triangle); + context.DrawGeometry(null, new Pen(Solarized.Brushes.Blue, 2), Triangle); } private void VerifyNoneVisualCache() {