config_tool: add support for searching

This adds a search box to the config tool to allow filtering properties
by keywords.

Resolves #1889.
This commit is contained in:
lahm86 2024-11-16 15:56:54 +00:00
parent 36eec3d72b
commit 1cbded12dc
15 changed files with 394 additions and 50 deletions

View file

@ -7,6 +7,7 @@
- added support for key/puzzle/pickup descriptions, allowing players to examine said items in the inventory (#1821)
- added an option to fix inventory item usage duplication (#1586)
- added optional automatic key/puzzle inventory item pre-selection (#1884)
- added a search feature to the config tool (#1889)
- changed OpenGL backend to use version 3.3, with fallback to 2.1 if initialization fails (#1738)
- changed text backend to accept named sequences. Currently supported sequences (limited by the sprites available in OG):
- `\{umlaut}`

View file

@ -2,6 +2,7 @@
- added support for custom levels to enforce values for any config setting (#1846)
- added an option to fix inventory item usage duplication (#1586)
- added optional automatic key/puzzle inventory item pre-selection (#1884)
- added a search feature to the config tool (#1889)
- fixed depth problems when drawing certain rooms (#1853, regression from 0.6)
- fixed Lara getting stuck in her hit animation if she is hit while mounting the boat or skidoo (#1606)
- fixed being unable to go from surface swimming to underwater swimming without first stopping (#1863, regression from 0.6)

View file

@ -58,55 +58,130 @@
<utils:RelayKeyBinding
CommandBinding="{Binding GitHubCommand}"
Key="F11" />
<utils:RelayKeyBinding
CommandBinding="{Binding BeginSearchCommand}"
Modifiers="Ctrl"
Key="F" />
</Window.InputBindings>
<DockPanel>
<Menu
DockPanel.Dock="Top"
Background="{DynamicResource {x:Static SystemColors.WindowBrush}}">
<MenuItem Header="{Binding ViewText[menu_file]}">
<MenuItem
Command="{Binding OpenCommand}"
Header="{Binding ViewText[command_open]}"
InputGestureText="Ctrl+O"/>
<MenuItem
Command="{Binding ReloadCommand}"
Header="{Binding ViewText[command_reload]}"
InputGestureText="F5"/>
<Separator/>
<MenuItem
Command="{Binding SaveCommand}"
Header="{Binding ViewText[command_save]}"
InputGestureText="Ctrl+S"/>
<MenuItem
Command="{Binding SaveAsCommand}"
Header="{Binding ViewText[command_save_as]}"
InputGestureText="Ctrl+Alt+S"/>
<Separator/>
<MenuItem
Command="{Binding ExitCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
Header="{Binding ViewText[command_exit]}"
InputGestureText="Alt+F4"/>
</MenuItem>
<DockPanel DockPanel.Dock="Top">
<Menu
Background="{DynamicResource {x:Static SystemColors.WindowBrush}}">
<MenuItem Header="{Binding ViewText[menu_file]}">
<MenuItem
Command="{Binding OpenCommand}"
Header="{Binding ViewText[command_open]}"
InputGestureText="Ctrl+O"/>
<MenuItem
Command="{Binding ReloadCommand}"
Header="{Binding ViewText[command_reload]}"
InputGestureText="F5"/>
<Separator/>
<MenuItem
Command="{Binding SaveCommand}"
Header="{Binding ViewText[command_save]}"
InputGestureText="Ctrl+S"/>
<MenuItem
Command="{Binding SaveAsCommand}"
Header="{Binding ViewText[command_save_as]}"
InputGestureText="Ctrl+Alt+S"/>
<Separator/>
<MenuItem
Command="{Binding ExitCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
Header="{Binding ViewText[command_exit]}"
InputGestureText="Alt+F4"/>
</MenuItem>
<MenuItem Header="{Binding ViewText[menu_tools]}">
<MenuItem
Command="{Binding RestoreDefaultsCommand}"
Header="{Binding ViewText[command_restore]}"/>
</MenuItem>
<MenuItem Header="{Binding ViewText[menu_tools]}">
<MenuItem
Command="{Binding RestoreDefaultsCommand}"
Header="{Binding ViewText[command_restore]}"/>
</MenuItem>
<MenuItem Header="{Binding ViewText[menu_help]}">
<MenuItem
Command="{Binding GitHubCommand}"
Header="{Binding ViewText[command_github]}"
InputGestureText="F11"/>
<Separator/>
<MenuItem
Command="{Binding AboutCommand}"
Header="{Binding ViewText[command_about]}"/>
</MenuItem>
</Menu>
<MenuItem Header="{Binding ViewText[menu_help]}">
<MenuItem
Command="{Binding GitHubCommand}"
Header="{Binding ViewText[command_github]}"
InputGestureText="F11"/>
<Separator/>
<MenuItem
Command="{Binding AboutCommand}"
Header="{Binding ViewText[command_about]}"/>
</MenuItem>
</Menu>
<Grid
DockPanel.Dock="Right"
Margin="0,7,7,0"
IsEnabled="{Binding IsEditorActive}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border
Grid.Column="1"
BorderBrush="#666"
BorderThickness="1"
Background="{Binding SearchFailStatus, Converter={utils:ConditionalMarkupConverter TrueValue='#FFC7CE', FalseValue='White'}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="24"/>
</Grid.ColumnDefinitions>
<TextBox
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
BorderThickness="0"
Background="Transparent"
Margin="0,1"
x:Name="SearchTermTextBox"
Text="{Binding Path=SearchText, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.BeginSearch}" Value="True">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=SearchTermTextBox}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<TextBlock
IsHitTestVisible="False"
Text="{Binding ViewText[label_search]}"
Padding="4,0,0,0"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Foreground="#666">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=SearchTermTextBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Button
Grid.Column="2"
Command="{Binding CloseSearchCommand}"
Style="{StaticResource SmallButtonStyle}"
IsEnabled="{Binding IsSearchTextDefined}"
Content="../Resources/close.png"
ToolTip="{Binding ViewText[command_close_search]}"/>
</Grid>
</Border>
</Grid>
</DockPanel>
<StatusBar
DockPanel.Dock="Bottom">
@ -163,7 +238,8 @@
IsEnabled="{Binding IsEditorActive}"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
SelectionChanged="TabControl_SelectionChanged">
SelectionChanged="TabControl_SelectionChanged"
Visibility="{Binding IsSearchActive, Converter={StaticResource InverseBoolToCollapsedConverter}}">
<TabControl.Resources>
<ResourceDictionary Source="/TRX_ConfigToolLib;component/Resources/styles.xaml" />
</TabControl.Resources>
@ -181,6 +257,34 @@
</TabControl.ContentTemplate>
</TabControl>
<TabControl
Grid.Row="1"
Margin="0,0,0,7"
Padding="0"
IsEnabled="{Binding IsEditorActive}"
ItemsSource="{Binding SearchResults}"
SelectedItem="{Binding SearchCategory, Mode=OneWay}"
Visibility="{Binding IsSearchActive, Converter={StaticResource BoolToCollapsedConverter}}">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ViewText[label_search_results]}"/>
<Button
Content="../Resources/close.png"
ToolTip="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ViewText[command_close_search]}"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.CloseSearchCommand}"
Style="{StaticResource TabButtonStyle}"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<controls:CategoryControl />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
<Grid
Grid.Row="2">
<Grid.ColumnDefinitions>

View file

@ -11,6 +11,7 @@ public class CategoryViewModel
public CategoryViewModel(Category category)
{
_category = category;
ItemsSource = new(category.Properties);
}
public string Title
@ -23,10 +24,7 @@ public class CategoryViewModel
get => AssemblyUtils.GetEmbeddedResourcePath(_category.Image ?? _defaultImage);
}
public IEnumerable<BaseProperty> ItemsSource
{
get => _category.Properties;
}
public FastObservableCollection<BaseProperty> ItemsSource { get; private set; }
public double ViewPosition { get; set; }
}

View file

@ -10,9 +10,12 @@ namespace TRX_ConfigToolLib.Models;
public class MainWindowViewModel : BaseLanguageViewModel
{
private const int _minSearchLength = 3;
private readonly Configuration _configuration;
public IEnumerable<CategoryViewModel> Categories { get; private set; }
public IEnumerable<CategoryViewModel> SearchResults { get; private set; }
public MainWindowViewModel()
{
@ -28,6 +31,17 @@ public class MainWindowViewModel : BaseLanguageViewModel
Categories = categories;
SelectedCategory = Categories.FirstOrDefault();
// Search results are contained within a special category whose properties are built
// on-the-fly. Pick a random picture for it each time the application is used.
Random random = new();
SearchCategory = new(new()
{
Properties = new(),
Image = _configuration.Categories[random.Next(_configuration.Categories.Count)].Image,
});
SearchResults = new List<CategoryViewModel> { SearchCategory };
}
private void EditorPropertyChanged(object sender, PropertyChangedEventArgs e)
@ -53,6 +67,8 @@ public class MainWindowViewModel : BaseLanguageViewModel
}
}
public CategoryViewModel SearchCategory { get; private set; }
private bool _isEditorDirty;
public bool IsEditorDirty
{
@ -230,6 +246,78 @@ public class MainWindowViewModel : BaseLanguageViewModel
return IsEditorActive;
}
private bool _beginSearch;
public bool BeginSearch
{
get => _beginSearch;
set
{
if (!value)
{
return;
}
_beginSearch = true;
NotifyPropertyChanged();
_beginSearch = false;
NotifyPropertyChanged();
}
}
private string _searchText = string.Empty;
public string SearchText
{
get => _searchText;
set
{
if (value == _searchText)
{
return;
}
_searchText = value;
RunPropertySearch();
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(IsSearchActive));
NotifyPropertyChanged(nameof(IsSearchTextDefined));
NotifyPropertyChanged(nameof(SearchFailStatus));
}
}
public bool IsSearchActive => SearchCategory.ItemsSource.Any();
public bool IsSearchTextDefined => _searchText.Length > 0;
public bool SearchFailStatus => _searchText.Trim().Length >= _minSearchLength && !IsSearchActive;
private void RunPropertySearch()
{
string text = _searchText.Trim().ToLower();
if (text.Length < _minSearchLength)
{
SearchCategory.ItemsSource.RemoveAll();
return;
}
text = TextUtilities.Normalise(text);
List<string> keywords = new(text.Split(null));
IEnumerable<BaseProperty> matchedProperties = Categories
.SelectMany(c => c.ItemsSource)
.Where(p => keywords.Any(p.NormalisedText.Contains));
SearchCategory.ItemsSource.ReplaceCollection(matchedProperties);
}
private RelayCommand _beginSearchCommand;
public ICommand BeginSearchCommand
{
get => _beginSearchCommand ??= new RelayCommand(() => BeginSearch = true);
}
private RelayCommand _closeSearchCommand;
public ICommand CloseSearchCommand
{
get => _closeSearchCommand ??= new RelayCommand(() => SearchText = string.Empty);
}
private RelayCommand _launchGameCommand;
public ICommand LaunchGameCommand
{

View file

@ -1,4 +1,6 @@
namespace TRX_ConfigToolLib.Models;
using TRX_ConfigToolLib.Utils;
namespace TRX_ConfigToolLib.Models;
public abstract class BaseProperty : BaseNotifyPropertyChanged
{
@ -16,6 +18,8 @@ public abstract class BaseProperty : BaseNotifyPropertyChanged
get => Language.Instance.Properties[Field].Description;
}
public string NormalisedText { get; private set; }
public abstract object ExportValue();
public abstract void LoadValue(string value);
public abstract void SetToDefault();
@ -41,5 +45,8 @@ public abstract class BaseProperty : BaseNotifyPropertyChanged
public virtual void Initialise(Specification specification)
{
SetToDefault();
string text = (Title + " " + Description).ToLower();
NormalisedText = TextUtilities.Normalise(text);
}
}

View file

@ -12,6 +12,9 @@
"menu_help": "_Help",
"command_github": "_GitHub",
"command_about": "_About",
"label_search": "Search...",
"label_search_results": "Search results",
"command_close_search": "Close search",
"label_locked_properties": "This configuration set contains some read-only values defined by the game author.",
"label_enforced": "Value enforced to:",
"checkbox_enabled": "Enabled",

View file

@ -12,6 +12,9 @@
"menu_help": "_Ayuda",
"command_github": "_GitHub",
"command_about": "_Acerca de",
"label_search": "Buscar...",
"label_search_results": "Resultados de búsqueda",
"command_close_search": "Cerrar la búsqueda",
"label_locked_properties": "Este conjunto de configuración contiene algunos valores de solo lectura definidos por el autor del juego.",
"label_enforced": "Valor aplicado a:",
"checkbox_enabled": "Habilitado",

View file

@ -12,6 +12,9 @@
"menu_help": "_Aide",
"command_github": "_GitHub",
"command_about": "_A propos",
"label_search": "Rechercher...",
"label_search_results": "Résultats de recherche",
"command_close_search": "Fermer la recherche",
"label_locked_properties": "Cet ensemble de configuration contient certaines valeurs en lecture seule définies par l'auteur du jeu.",
"label_enforced": "Valeur imposée à:",
"checkbox_enabled": "Activé",

View file

@ -12,6 +12,9 @@
"menu_help": "_Aiuto",
"command_github": "_GitHub",
"command_about": "_Informazioni su",
"label_search": "Ricerca...",
"label_search_results": "Risultati della ricerca",
"command_close_search": "Chiudi la ricerca",
"label_locked_properties": "Questo set di configurazione contiene alcuni valori di sola lettura definiti dall'autore del gioco.",
"label_enforced": "Valore applicato a:",
"checkbox_enabled": "Abilitato",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -225,4 +225,70 @@
</Trigger>
</Style.Triggers>
</Style>
<Style
TargetType="{x:Type Button}"
x:Key="SmallButtonStyle">
<Setter
Property="Background"
Value="#333"/>
<Setter
Property="Foreground"
Value="#FFF"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type Button}">
<Border
Background="{TemplateBinding Background}"
Padding="0,2"
BorderThickness="0"
BorderBrush="#ABADB3">
<Image
Source="{Binding Content, RelativeSource={RelativeSource AncestorType=Button}}"
Height="6"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter
Property="Background"
Value="#121212"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter
Property="Background"
Value="#858585"/>
</Trigger>
</Style.Triggers>
</Style>
<Style
TargetType="{x:Type Button}"
BasedOn="{StaticResource SmallButtonStyle}"
x:Key="TabButtonStyle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type Button}">
<Border
Background="{TemplateBinding Background}"
Padding="6,2"
Margin="4,0,0,0"
BorderThickness="1"
BorderBrush="#ABADB3">
<Image
Source="../Resources/close.png"
Height="6"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -9,6 +9,7 @@
<ItemGroup>
<None Remove="Resources\arrow-down.png" />
<None Remove="Resources\arrow-up.png" />
<None Remove="Resources\close.png" />
<None Remove="Resources\const.json" />
<None Remove="Resources\Lang\en.json" />
<None Remove="Resources\Lang\es.json" />
@ -28,6 +29,7 @@
<ItemGroup>
<Resource Include="Resources\arrow-down.png" />
<Resource Include="Resources\arrow-up.png" />
<Resource Include="Resources\close.png" />
<Resource Include="Resources\styles.xaml">
<Generator>MSBuild:Compile</Generator>
</Resource>

View file

@ -0,0 +1,51 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace TRX_ConfigToolLib.Utils;
public class FastObservableCollection<T> : ObservableCollection<T>
{
public FastObservableCollection()
: base() { }
public FastObservableCollection(IEnumerable<T> collection)
: base(collection) { }
public virtual void RemoveAll()
{
if (Items.Any())
{
Items.Clear();
}
}
public virtual void ReplaceCollection(IEnumerable<T> collection)
{
if (collection == null || collection.SequenceEqual(Items))
{
return;
}
if (!collection.Any())
{
Clear();
return;
}
Items.Clear();
int oldCount = Count;
foreach (T item in collection)
{
Items.Add(item);
}
if (Count != oldCount)
{
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
}
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}

View file

@ -0,0 +1,14 @@
using System.Globalization;
using System.Text;
namespace TRX_ConfigToolLib.Utils;
public static class TextUtilities
{
public static string Normalise(string s)
{
return new string(s.Normalize(NormalizationForm.FormD)
.Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
.ToArray());
}
}