diff --git a/Il2CppInspector.GUI/PluginConfigurationDialog.xaml b/Il2CppInspector.GUI/PluginConfigurationDialog.xaml index ff877d7..b5c165a 100644 --- a/Il2CppInspector.GUI/PluginConfigurationDialog.xaml +++ b/Il2CppInspector.GUI/PluginConfigurationDialog.xaml @@ -1,143 +1,52 @@ - + SizeToContent="Height" MaxHeight="800" + Closing="Window_Closing"> - - + + - - - - - - - - - - + + - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -167,13 +183,15 @@ Content="{Binding Plugin.Name}" ContentStringFormat="Configuration for {0}" Padding="8,0,0,0"/> - + + ChoiceDropdownTemplate="{StaticResource ChoiceDropdownTemplate}" + ChoiceListTemplate="{StaticResource ChoiceListTemplate}"> diff --git a/Il2CppInspector.GUI/PluginConfigurationDialog.xaml.cs b/Il2CppInspector.GUI/PluginConfigurationDialog.xaml.cs index 81033a4..efe25e6 100644 --- a/Il2CppInspector.GUI/PluginConfigurationDialog.xaml.cs +++ b/Il2CppInspector.GUI/PluginConfigurationDialog.xaml.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Text; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; @@ -16,30 +17,33 @@ using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; using Microsoft.Win32; -using Il2CppInspectorGUI; using System.Windows.Forms.Design; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Linq; using Il2CppInspector.PluginAPI.V100; -using static Il2CppInspector.PluginManager; +using Il2CppInspector.Reflection; -namespace Il2CppInspector.GUI -{ +namespace Il2CppInspectorGUI +{ // Class which selects the correct control to display for each plugin option public class OptionTemplateSelector : DataTemplateSelector { public DataTemplate TextTemplate { get; set; } public DataTemplate FilePathTemplate { get; set; } - public DataTemplate NumberTemplate { get; set; } + public DataTemplate NumberDecimalTemplate { get; set; } + public DataTemplate NumberHexTemplate { get; set; } public DataTemplate BooleanTemplate { get; set; } - public DataTemplate ChoiceTemplate { get; set; } + public DataTemplate ChoiceDropdownTemplate { get; set; } + public DataTemplate ChoiceListTemplate { get; set; } // Use some fancy reflection to get the right template property + // If the plugin option is PluginOptionFooBar and its style enum property is Baz, the template will be FooBarBazTemplate public override DataTemplate SelectTemplate(object item, DependencyObject container) { var option = (IPluginOption) item; - return (DataTemplate) GetType().GetProperty(option.GetType().Name.Split("`")[0]["PluginOption".Length..] + "Template").GetValue(this); + var style = item.GetType().GetProperty("Style")?.GetValue(item).ToString() ?? string.Empty; + return (DataTemplate) GetType().GetProperty(option.GetType().Name.Split("`")[0]["PluginOption".Length..] + style + "Template").GetValue(this); } } @@ -51,14 +55,76 @@ namespace Il2CppInspector.GUI // Item to configure public IPlugin Plugin { get; } + // This helps us find XAML elements withing a DataTemplate + // Adapted from https://docs.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-find-datatemplate-generated-elements?view=netframeworkdesktop-4.8 + private childItem FindVisualChild(DependencyObject obj) where childItem : DependencyObject { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++) { + DependencyObject child = VisualTreeHelper.GetChild(obj, i); + if (child != null && child is childItem) { + return (childItem) child; + } else { + childItem childOfChild = FindVisualChild(child); + if (childOfChild != null) + return childOfChild; + } + } + return null; + } + + // Adapted from https://stackoverflow.com/a/565560 + // The dependency object is valid if it has no errors and all + // of its children (that are dependency objects) are error-free. + private bool IsValid(DependencyObject obj) + => !Validation.GetHasError(obj) && Enumerable.Range(0, VisualTreeHelper.GetChildrenCount(obj)).All(n => IsValid(VisualTreeHelper.GetChild(obj, n))); + + // Adapted from https://stackoverflow.com/questions/22510428/get-listboxitem-from-listbox + // In order to force validation with ExceptionValidationRule when the window opens, + // we need to wait until all of the ListBoxItems are populated, then find the element with the value + // then force WPF to try to update the source property to see if it raises an exception + // and cause the DataTriggers to execute. + // This relies on a 'valueControl' named element existing. + void OptionsListBoxStatusChanged(object sender, EventArgs e) { + // Wait for items to be generated + if (lstOptions.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated) + return; + + // Remove event + lstOptions.ItemContainerGenerator.StatusChanged -= OptionsListBoxStatusChanged; + + // your items are now generated + + // Adapted from https://stackoverflow.com/a/18008545 + foreach (var item in lstOptions.Items) { + var listBoxItem = lstOptions.ItemContainerGenerator.ContainerFromItem(item); + var presenter = FindVisualChild(listBoxItem); + + var dataTemplate = presenter.ContentTemplateSelector.SelectTemplate(item, listBoxItem); + var boundControl = dataTemplate.FindName("valueControl", presenter); + + // Adapted from https://stackoverflow.com/questions/794370/update-all-bindings-in-usercontrol-at-once + ((FrameworkElement) boundControl).GetBindingExpression(boundControl switch { + TextBox t => TextBox.TextProperty, + CheckBox c => CheckBox.IsCheckedProperty, + ListBox l => ListBox.SelectedValueProperty, + ComboBox m => ComboBox.SelectedValueProperty, + TextBlock b => TextBlock.TextProperty, + _ => throw new InvalidOperationException("Unknown value control type") + }).UpdateSource(); + } + } + + // Initialize configuration dialog window public PluginConfigurationDialog(IPlugin plugin) { InitializeComponent(); DataContext = this; Plugin = plugin; + + // Validate options once they have loaded + lstOptions.ItemContainerGenerator.StatusChanged += OptionsListBoxStatusChanged; } private void okButton_Click(object sender, RoutedEventArgs e) { - // Closes dialog box automatically + // Close dialog box but call OnClosing first to validate all the options DialogResult = true; // TODO: Plugin hook OptionsChanged (and make sure it works when clicking close icon as well) @@ -73,17 +139,21 @@ namespace Il2CppInspector.GUI Title = option.Description, Filter = "All files (*.*)|*.*", FileName = option.Value, - CheckFileExists = true + CheckFileExists = false, + CheckPathExists = false }; if (openFileDialog.ShowDialog() == true) { - option.Value = openFileDialog.FileName; - // This spaghetti saves us from implementing INotifyPropertyChanged on Plugin.Options // (we don't want to expose WPF stuff in our SDK) // Will break if we change the format of the FilePathDataTemplate too much - var tb = ((DockPanel) ((Button) sender).Parent).Children.OfType().First(n => n.Name == "txtFilePathSelector"); - tb.Text = option.Value; + var tb = ((DockPanel) ((Button) sender).Parent).Children.OfType().First().Children.OfType().First(n => n.Name == "valueControl"); + try { + tb.Clear(); + tb.AppendText(openFileDialog.FileName); + tb.GetBindingExpression(TextBox.TextProperty).UpdateSource(); + } + catch { } } } @@ -91,5 +161,13 @@ namespace Il2CppInspector.GUI private void txtHexString_PreviewTextInput(object sender, TextCompositionEventArgs e) { e.Handled = !Regex.IsMatch(e.Text, @"[A-Fa-f0-9]"); } + + // Check options validity before allowing the dialog to close either by clicking OK or the close icon + private void Window_Closing(object sender, CancelEventArgs e) { + if (!IsValid(lstOptions)) { + MessageBox.Show("One or more options are invalid.", "Il2CppInspector Plugin Configuration"); + e.Cancel = true; + } + } } } diff --git a/Il2CppInspector.GUI/PluginManagerDialog.xaml.cs b/Il2CppInspector.GUI/PluginManagerDialog.xaml.cs index 0788a9f..bef4fda 100644 --- a/Il2CppInspector.GUI/PluginManagerDialog.xaml.cs +++ b/Il2CppInspector.GUI/PluginManagerDialog.xaml.cs @@ -46,7 +46,7 @@ namespace Il2CppInspector.GUI private void btnConfig_Click(object sender, RoutedEventArgs e) { var plugin = (ManagedPlugin) ((Button) sender).DataContext; - var configDlg = new PluginConfigurationDialog(plugin.Plugin); + var configDlg = new Il2CppInspectorGUI.PluginConfigurationDialog(plugin.Plugin); configDlg.Owner = this; configDlg.ShowDialog(); }