File: Options\AbstractOptionPageControl.cs
Web Access
Project: src\src\VisualStudio\Core\Impl\Microsoft.VisualStudio.LanguageServices.Implementation.csproj (Microsoft.VisualStudio.LanguageServices.Implementation)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServices.Implementation.Options.Converters;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Options;
 
[DesignerCategory("code")] // this must be fully qualified
public abstract class AbstractOptionPageControl : UserControl
{
    internal readonly OptionStore OptionStore;
    private readonly List<BindingExpressionBase> _bindingExpressions = [];
    private readonly List<OptionPageSearchHandler> _searchHandlers = [];
 
    private protected AbstractOptionPageControl(OptionStore optionStore)
    {
        InitializeStyles();
 
        if (DesignerProperties.GetIsInDesignMode(this))
        {
            return;
        }
 
        this.OptionStore = optionStore;
    }
 
    private void InitializeStyles()
    {
        var groupBoxStyle = new System.Windows.Style(typeof(GroupBox));
        groupBoxStyle.Setters.Add(new Setter(GroupBox.PaddingProperty, new Thickness() { Left = 7, Right = 7, Top = 7 }));
        groupBoxStyle.Setters.Add(new Setter(GroupBox.MarginProperty, new Thickness() { Bottom = 3 }));
        groupBoxStyle.Setters.Add(new Setter(GroupBox.ForegroundProperty, new DynamicResourceExtension(SystemColors.WindowTextBrushKey)));
        Resources.Add(typeof(GroupBox), groupBoxStyle);
 
        var checkBoxStyle = new System.Windows.Style(typeof(CheckBox));
        checkBoxStyle.Setters.Add(new Setter(CheckBox.MarginProperty, new Thickness() { Bottom = 7 }));
        checkBoxStyle.Setters.Add(new Setter(CheckBox.ForegroundProperty, new DynamicResourceExtension(SystemColors.WindowTextBrushKey)));
        Resources.Add(typeof(CheckBox), checkBoxStyle);
 
        var textBoxStyle = new System.Windows.Style(typeof(TextBox));
        textBoxStyle.Setters.Add(new Setter(TextBox.MarginProperty, new Thickness() { Left = 7, Right = 7 }));
        textBoxStyle.Setters.Add(new Setter(TextBox.ForegroundProperty, new DynamicResourceExtension(SystemColors.WindowTextBrushKey)));
        Resources.Add(typeof(TextBox), textBoxStyle);
 
        var radioButtonStyle = new System.Windows.Style(typeof(RadioButton));
        radioButtonStyle.Setters.Add(new Setter(RadioButton.MarginProperty, new Thickness() { Bottom = 7 }));
        radioButtonStyle.Setters.Add(new Setter(RadioButton.ForegroundProperty, new DynamicResourceExtension(SystemColors.WindowTextBrushKey)));
        Resources.Add(typeof(RadioButton), radioButtonStyle);
 
        var comboBoxStyle = new System.Windows.Style(typeof(ComboBox));
        comboBoxStyle.Setters.Add(new Setter(ComboBox.MarginProperty, new Thickness() { Bottom = 7 }));
        comboBoxStyle.Setters.Add(new Setter(ComboBox.ForegroundProperty, new DynamicResourceExtension(SystemColors.WindowTextBrushKey)));
        Resources.Add(typeof(ComboBox), comboBoxStyle);
    }
 
    private protected void BindToOption(CheckBox checkbox, Option2<bool> optionKey)
    {
        var binding = new Binding()
        {
            Source = new OptionBinding<bool>(OptionStore, optionKey),
            Path = new PropertyPath("Value"),
            UpdateSourceTrigger = UpdateSourceTrigger.Default
        };
 
        AddSearchHandler(checkbox);
 
        var bindingExpression = checkbox.SetBinding(CheckBox.IsCheckedProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption(CheckBox checkbox, Option2<bool?> nullableOptionKey, Func<bool> onNullValue)
    {
        var binding = new Binding()
        {
            Source = new OptionBinding<bool?>(OptionStore, nullableOptionKey),
            Path = new PropertyPath("Value"),
            UpdateSourceTrigger = UpdateSourceTrigger.Default,
            Converter = new NullableBoolOptionConverter(onNullValue)
        };
 
        AddSearchHandler(checkbox);
 
        var bindingExpression = checkbox.SetBinding(CheckBox.IsCheckedProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption(CheckBox checkbox, PerLanguageOption2<bool> optionKey, string languageName)
    {
        var binding = new Binding()
        {
            Source = new PerLanguageOptionBinding<bool>(OptionStore, optionKey, languageName),
            Path = new PropertyPath("Value"),
            UpdateSourceTrigger = UpdateSourceTrigger.Default
        };
 
        AddSearchHandler(checkbox);
 
        var bindingExpression = checkbox.SetBinding(CheckBox.IsCheckedProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption(CheckBox checkbox, PerLanguageOption2<bool?> nullableOptionKey, string languageName, Func<bool> onNullValue)
    {
        var binding = new Binding()
        {
            Source = new PerLanguageOptionBinding<bool?>(OptionStore, nullableOptionKey, languageName),
            Path = new PropertyPath("Value"),
            UpdateSourceTrigger = UpdateSourceTrigger.Default,
            Converter = new NullableBoolOptionConverter(onNullValue)
        };
 
        AddSearchHandler(checkbox);
 
        var bindingExpression = checkbox.SetBinding(CheckBox.IsCheckedProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption(TextBox textBox, Option2<int> optionKey)
    {
        var binding = new Binding()
        {
            Source = new OptionBinding<int>(OptionStore, optionKey),
            Path = new PropertyPath("Value"),
            UpdateSourceTrigger = UpdateSourceTrigger.Default
        };
 
        var bindingExpression = textBox.SetBinding(TextBox.TextProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption(TextBox textBox, PerLanguageOption2<int> optionKey, string languageName)
    {
        var binding = new Binding()
        {
            Source = new PerLanguageOptionBinding<int>(OptionStore, optionKey, languageName),
            Path = new PropertyPath("Value"),
            UpdateSourceTrigger = UpdateSourceTrigger.Default
        };
 
        var bindingExpression = textBox.SetBinding(TextBox.TextProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption<T>(ComboBox comboBox, Option2<T> optionKey, ContentControl label = null)
    {
        var binding = new Binding()
        {
            Source = new OptionBinding<T>(OptionStore, optionKey),
            Path = new PropertyPath("Value"),
            Converter = new ComboBoxItemTagToIndexConverter(),
            ConverterParameter = comboBox
        };
 
        AddSearchHandler(comboBox);
 
        if (label is not null)
            AddSearchHandler(label);
 
        var bindingExpression = comboBox.SetBinding(ComboBox.SelectedIndexProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption<T>(ComboBox comboBox, PerLanguageOption2<T> optionKey, string languageName, ContentControl label = null)
    {
        var binding = new Binding()
        {
            Source = new PerLanguageOptionBinding<T>(OptionStore, optionKey, languageName),
            Path = new PropertyPath("Value"),
            Converter = new ComboBoxItemTagToIndexConverter(),
            ConverterParameter = comboBox
        };
 
        AddSearchHandler(comboBox);
 
        if (label is not null)
            AddSearchHandler(label);
 
        var bindingExpression = comboBox.SetBinding(ComboBox.SelectedIndexProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption<T>(RadioButton radiobutton, Option2<T> optionKey, T optionValue)
    {
        var binding = new Binding()
        {
            Source = new OptionBinding<T>(OptionStore, optionKey),
            Path = new PropertyPath("Value"),
            UpdateSourceTrigger = UpdateSourceTrigger.Default,
            Converter = new RadioButtonCheckedConverter(),
            ConverterParameter = optionValue
        };
 
        AddSearchHandler(radiobutton);
 
        var bindingExpression = radiobutton.SetBinding(RadioButton.IsCheckedProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption<T>(RadioButton radioButton, Option2<T?> nullableOptionKey, T optionValue, Func<bool> onNullValue) where T : struct
    {
        var binding = new Binding()
        {
            Source = new OptionBinding<T?>(OptionStore, nullableOptionKey),
            Path = new PropertyPath("Value"),
            UpdateSourceTrigger = UpdateSourceTrigger.Default,
            Converter = new RadioButtonCheckedConverter<T>(onNullValue),
            ConverterParameter = optionValue,
        };
 
        AddSearchHandler(radioButton);
 
        var bindingExpression = radioButton.SetBinding(RadioButton.IsCheckedProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    private protected void BindToOption<T>(RadioButton radiobutton, PerLanguageOption2<T> optionKey, T optionValue, string languageName)
    {
        var binding = new Binding()
        {
            Source = new PerLanguageOptionBinding<T>(OptionStore, optionKey, languageName),
            Path = new PropertyPath("Value"),
            UpdateSourceTrigger = UpdateSourceTrigger.Default,
            Converter = new RadioButtonCheckedConverter(),
            ConverterParameter = optionValue
        };
 
        AddSearchHandler(radiobutton);
 
        var bindingExpression = radiobutton.SetBinding(RadioButton.IsCheckedProperty, binding);
        _bindingExpressions.Add(bindingExpression);
    }
 
    internal virtual void OnLoad()
    {
        foreach (var bindingExpression in _bindingExpressions)
        {
            bindingExpression.UpdateTarget();
        }
    }
 
    internal virtual void OnSave()
    {
    }
 
    internal virtual void Close()
    {
    }
 
    internal virtual void OnSearch(string searchString)
    {
        var shouldScrollIntoView = true;
        foreach (var handler in _searchHandlers)
        {
            if (handler.TryHighlightSearchString(searchString) && shouldScrollIntoView)
            {
                handler.EnsureVisible();
                shouldScrollIntoView = false;
            }
        }
    }
 
    private protected void AddSearchHandler(ComboBox comboBox)
    {
        foreach (ComboBoxItem item in comboBox.Items)
        {
            AddSearchHandler(item);
        }
    }
 
    private protected void AddSearchHandler(ContentControl control)
    {
        Debug.Assert(control.Content is string, $"I don't know how to add keyword search support for the '{control.GetType().Name}' control with content type '{control.Content?.GetType().Name ?? "null"}'");
        if (control.Content is string content)
        {
            _searchHandlers.Add(new OptionPageSearchHandler(control, content));
        }
    }
}
 
public sealed class RadioButtonCheckedConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        => value.Equals(parameter);
 
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        => value.Equals(true) ? parameter : Binding.DoNothing;
}
 
public sealed class RadioButtonCheckedConverter<T>(Func<bool> onNullValue) : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        => value switch
        {
            null => onNullValue(),
            _ => value.Equals(parameter),
        };
 
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        => value.Equals(true) ? parameter : Binding.DoNothing;
}
 
public class ComboBoxItemTagToIndexConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var comboBox = (ComboBox)parameter;
 
        for (var index = 0; index < comboBox.Items.Count; index++)
        {
            var item = (ComboBoxItem)comboBox.Items[index];
            if (item.Tag.Equals(value))
            {
                return index;
            }
        }
 
        return -1;
    }
 
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var index = (int)value;
        if (index == -1)
        {
            return null;
        }
 
        var comboBox = (ComboBox)parameter;
        var item = (ComboBoxItem)comboBox.Items[index];
        return item.Tag;
    }
}