Add a search field to the text tool

The unit test and search logic implementation were actually pretty simple. However, WPF TextBox's don't natively support a way to show text highlight when the textbox doesn't have focus... so I had to implemnet my own focus. I did this by wrapping the TextBox in a Grid so I could overlay a Rectangle that uses the same highlight color.

See TabView.xaml.cs for all the mathy bits.
This commit is contained in:
Benjamin Popp
2019-10-26 15:05:32 -05:00
parent 6827faa013
commit 11cbc1ba1e
4 changed files with 137 additions and 9 deletions

View File

@@ -13,6 +13,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
private readonly IToolTrayViewModel runner;
private readonly StubCommand
checkIsText = new StubCommand(),
search = new StubCommand(),
insertText = new StubCommand();
// if we're in the middle of updating ourselves, we may notify changes to other controls.
@@ -37,16 +38,24 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
}
}
private string searchText;
public string SearchText {
get => searchText;
set => TryUpdate(ref searchText, value);
}
public ICommand CheckIsText => checkIsText;
public ICommand Search => search;
public ICommand InsertText => insertText;
private bool showMessage;
private bool showMessage = true;
public bool ShowMessage {
get => showMessage;
set {
if (TryUpdate(ref showMessage, value)) {
checkIsText.CanExecuteChanged?.Invoke(checkIsText, EventArgs.Empty);
insertText.CanExecuteChanged?.Invoke(checkIsText, EventArgs.Empty);
search.CanExecuteChanged?.Invoke(checkIsText, EventArgs.Empty);
}
}
}
@@ -129,6 +138,21 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
Enabled = true;
ShowMessage = false;
};
search.CanExecute = arg => !ShowMessage;
search.Execute = arg => {
var target = searchText.ToLower();
if (target.Length == 0) return;
var content = Content.ToLower();
var start = Math.Min(Content.Length - target.Length, contentIndex);
var index = content.Substring(start + 1).IndexOf(target);
if (index < 0) index = content.IndexOf(target);
else index += start + 1;
if (index < 0) return;
ContentIndex = index;
ContentSelectionLength = target.Length;
};
}
public void RefreshContentAtAddress() {

View File

@@ -494,6 +494,23 @@ namespace HavenSoft.HexManiac.Tests {
Assert.Equal("000030", test.ViewPort.Headers[0]);
}
[Fact]
public void TextToolSearchTest() {
var test = new BaseViewModelTestClass();
test.ViewPort.Edit("FF @00 ^text\"\" Hello World Worble!");
test.ViewPort.SelectionStart = new Point();
var tool = test.ViewPort.Tools.StringTool;
tool.SearchText = "wor";
tool.Search.Execute();
Assert.Equal(6, tool.ContentIndex);
Assert.Equal(3, tool.ContentSelectionLength);
tool.Search.Execute();
Assert.Equal(12, tool.ContentIndex);
Assert.Equal(3, tool.ContentSelectionLength);
}
private static readonly ThumbParser parser;
static ToolTests(){
parser = new ThumbParser(File.ReadAllLines("Models/Code/armReference.txt"));

View File

@@ -4,7 +4,8 @@
xmlns:hsv="clr-namespace:HavenSoft.HexManiac.WPF.Resources"
xmlns:hsg3hvmtr="clr-namespace:HavenSoft.HexManiac.Core.ViewModels.Tools;assembly=HexManiac.Core"
xmlns:hsg3ww="clr-namespace:HavenSoft.HexManiac.WPF.Windows"
xmlns:hsg3hv="clr-namespace:HavenSoft.HexManiac.WPF.Controls">
xmlns:hsg3hv="clr-namespace:HavenSoft.HexManiac.WPF.Controls"
DataContextChanged="HandleDataContextChanged">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
<hsg3hv:IntegerToHexConverter x:Key="Hex"/>
@@ -85,10 +86,26 @@
<Button Margin="0,3" Command="{Binding StringTool.CheckIsText}" Content="Show As Text" HorizontalAlignment="Right" DockPanel.Dock="Top" Visibility="{Binding StringTool.ShowMessage, Converter={StaticResource BoolToVisibility}}"/>
<Button Command="{Binding StringTool.InsertText}" Content="Insert New Text" HorizontalAlignment="Right" DockPanel.Dock="Top" Visibility="{Binding StringTool.ShowMessage, Converter={StaticResource BoolToVisibility}}"/>
<TextBlock Text="Content:" Margin="5,30,0,0" DockPanel.Dock="Top"/>
<TextBox Margin="5,5" AcceptsReturn="True" MinHeight="50" Width="255" TextWrapping="Wrap" FontFamily="Consolas" VerticalScrollBarVisibility="Auto" UndoLimit="0"
SelectionChanged="StringToolContentSelectionChanged"
IsEnabled="{Binding StringTool.Enabled}"
Text="{Binding StringTool.Content, UpdateSourceTrigger=PropertyChanged}" />
<Button Command="{Binding StringTool.Search}" Margin="5,0" Content="Search" HorizontalAlignment="Right" DockPanel.Dock="Bottom" />
<TextBox Margin="5,0" Width="255" FontFamily="Consolas" Text="{Binding StringTool.SearchText, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding StringTool.Enabled}" DockPanel.Dock="Bottom">
<TextBox.InputBindings>
<KeyBinding Key="Return" Command="{Binding StringTool.Search}"/>
<KeyBinding Key="F3" Command="{Binding StringTool.Search}"/>
</TextBox.InputBindings>
</TextBox>
<TextBlock Text="Search:" Margin="5,0" DockPanel.Dock="Bottom"/>
<Grid Margin="5,5" MinHeight="50" Width="255">
<TextBox Name="StringToolTextBox" AcceptsReturn="True" TextWrapping="Wrap" FontFamily="Consolas" VerticalScrollBarVisibility="Auto" UndoLimit="0"
SelectionChanged="StringToolContentSelectionChanged"
LostFocus="ShowManualSelection"
GotFocus="HideManualSelection"
ScrollViewer.ScrollChanged="UpdateManualSelectionFromScroll"
IsEnabled="{Binding StringTool.Enabled}"
Text="{Binding StringTool.Content, UpdateSourceTrigger=PropertyChanged}" />
<Rectangle Name="ManualHighlight" HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="14" Fill="{DynamicResource Accent}" Opacity=".4"/>
</Grid>
</DockPanel>
<DockPanel Width="265" Name="TableToolPanel">
<DockPanel.Style>

View File

@@ -1,7 +1,9 @@
using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core;
using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.Tools;
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
@@ -28,12 +30,80 @@ namespace HavenSoft.HexManiac.WPF.Controls {
CodeModeSelector.ItemsSource = Enum.GetValues(typeof(CodeMode)).Cast<CodeMode>();
}
#region Manual Selection Code
private void HideManualSelection(object sender, EventArgs e) => ManualHighlight.Visibility = Visibility.Collapsed;
private void ShowManualSelection(object sender, EventArgs e) {
ManualHighlight.Visibility = Visibility.Visible;
UpdateManualSelectionFromScroll(StringToolTextBox, EventArgs.Empty);
}
private void HandleDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) {
if (e.OldValue is ViewPort viewPortOld) {
viewPortOld.Tools.StringTool.PropertyChanged -= HandleStringToolPropertyChanged;
}
if (e.NewValue is ViewPort viewPortNew) {
viewPortNew.Tools.StringTool.PropertyChanged += HandleStringToolPropertyChanged;
}
}
private void HandleStringToolPropertyChanged(object sender, PropertyChangedEventArgs e) {
var tool = (PCSTool)sender;
StringToolTextBox.SelectionChanged -= StringToolContentSelectionChanged;
if (e.PropertyName is nameof(PCSTool.ContentIndex)) {
StringToolTextBox.SelectionStart = tool.ContentIndex;
UpdateManualSelection(tool);
}
if (e.PropertyName is nameof(PCSTool.ContentSelectionLength)) {
StringToolTextBox.SelectionLength = tool.ContentSelectionLength;
UpdateManualSelection(tool);
}
StringToolTextBox.SelectionChanged += StringToolContentSelectionChanged;
}
private void UpdateManualSelection(PCSTool tool) {
var linesBeforeSelection = StringToolTextBox.Text.Substring(0, StringToolTextBox.SelectionStart).Split(Environment.NewLine).Length - 1;
var totalLines = StringToolTextBox.Text.Split(Environment.NewLine).Length;
var highestScroll = StringToolTextBox.ExtentHeight - StringToolTextBox.ViewportHeight;
var verticalOffset = linesBeforeSelection * highestScroll / totalLines;
StringToolTextBox.ScrollToVerticalOffset(verticalOffset);
}
private void UpdateManualSelectionFromScroll(object sender, EventArgs e) {
if (ManualHighlight.Visibility != Visibility.Visible) return;
var tools = StringToolTextBox.DataContext as ToolTray;
if (tools == null || tools.StringTool == null) return;
var tool = tools.StringTool;
var linesBeforeSelection = StringToolTextBox.Text.Substring(0, StringToolTextBox.SelectionStart).Split(Environment.NewLine).Length - 1;
var totalLines = StringToolTextBox.Text.Split(Environment.NewLine).Length;
var verticalOffset = StringToolTextBox.VerticalOffset;
var lineHeight = StringToolTextBox.ExtentHeight / totalLines;
var verticalStart = lineHeight * linesBeforeSelection - verticalOffset + 2;
if (verticalStart < 0 || verticalStart > StringToolTextBox.ViewportHeight) return;
var selectionStart = StringToolTextBox.Text.Substring(0, StringToolTextBox.SelectionStart).Split(Environment.NewLine).Last().Length;
const int fontWidth = 7; // FormattedText.Width gives more like 6.5, but 7 actually looks better.
var horizontalStart = selectionStart * fontWidth + 2;
var width = tool.ContentSelectionLength * fontWidth;
ManualHighlight.Margin = new Thickness(horizontalStart, verticalStart, StringToolTextBox.ActualWidth - horizontalStart - width, 0);
}
#endregion
private void StringToolContentSelectionChanged(object sender, RoutedEventArgs e) {
var textbox = (TextBox)sender;
var tools = textbox.DataContext as ToolTray;
if (tools == null || tools.StringTool == null) return;
tools.StringTool.ContentIndex = textbox.SelectionStart;
tools.StringTool.ContentSelectionLength = textbox.SelectionLength;
var tool = tools.StringTool;
tool.PropertyChanged -= HandleStringToolPropertyChanged;
tool.ContentIndex = textbox.SelectionStart;
tool.ContentSelectionLength = textbox.SelectionLength;
tool.PropertyChanged += HandleStringToolPropertyChanged;
}
private void HeaderMouseDown(object sender, MouseButtonEventArgs e) {