Browse Source

Fix #3251: Decompiler Settings: CheckBox in group header does not reflect state of the group

pull/3252/head
tom-englert 2 years ago
parent
commit
d435f5ffaf
  1. 50
      ILSpy/Options/DecompilerSettingsPanel.xaml
  2. 59
      ILSpy/Options/DecompilerSettingsPanel.xaml.cs
  3. 101
      ILSpy/Options/DecompilerSettingsViewModel.cs

50
ILSpy/Options/DecompilerSettingsPanel.xaml

@ -7,13 +7,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:options="clr-namespace:ICSharpCode.ILSpy.Options" xmlns:options="clr-namespace:ICSharpCode.ILSpy.Options"
d:DataContext="{d:DesignInstance options:DecompilerSettingsViewModel}"> d:DataContext="{d:DesignInstance options:DecompilerSettingsViewModel}">
<UserControl.Resources>
<CollectionViewSource x:Key="SettingsCollection" Source="{Binding Settings}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Category" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</UserControl.Resources>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@ -24,33 +17,26 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Margin="3" Grid.ColumnSpan="3" TextWrapping="Wrap" Text="{x:Static properties:Resources.DecompilerSettingsPanelLongText}" /> <TextBlock Margin="3" Grid.ColumnSpan="3" TextWrapping="Wrap" Text="{x:Static properties:Resources.DecompilerSettingsPanelLongText}" />
<ScrollViewer Grid.Row="1"> <ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding Source={StaticResource SettingsCollection}}"> <ItemsControl ItemsSource="{Binding Settings}">
<ItemsControl.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}" BasedOn="{StaticResource {x:Type GroupItem}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupItem">
<Expander Padding="0" BorderThickness="0" IsExpanded="True">
<Expander.Header>
<CheckBox Checked="OnGroupChecked" Unchecked="OnGroupUnchecked" Loaded="OnGroupLoaded" VerticalContentAlignment="Center"
FontSize="16" FontWeight="Bold"
d:DataContext="{d:DesignInstance GroupItem}"
Content="{Binding Name}" />
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ItemsControl.GroupStyle>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<CheckBox Margin="19,0,0,0" IsChecked="{Binding IsEnabled}" Content="{Binding Description}" /> <Grid>
<Expander Padding="0" BorderThickness="0" IsExpanded="True">
<Expander.Header>
<CheckBox VerticalContentAlignment="Center"
FontSize="16" FontWeight="Bold"
Content="{Binding Category}"
IsChecked="{Binding AreAllItemsChecked, Mode=TwoWay}"/>
</Expander.Header>
<ItemsControl ItemsSource="{Binding Settings}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Margin="28,0,0,0" IsChecked="{Binding IsEnabled}" Content="{Binding Description}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
</Grid>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>

59
ILSpy/Options/DecompilerSettingsPanel.xaml.cs

@ -16,12 +16,6 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Xml.Linq; using System.Xml.Linq;
using ICSharpCode.ILSpyX.Settings; using ICSharpCode.ILSpyX.Settings;
@ -32,7 +26,7 @@ namespace ICSharpCode.ILSpy.Options
/// Interaction logic for DecompilerSettingsPanel.xaml /// Interaction logic for DecompilerSettingsPanel.xaml
/// </summary> /// </summary>
[ExportOptionPage(Title = nameof(Properties.Resources.Decompiler), Order = 10)] [ExportOptionPage(Title = nameof(Properties.Resources.Decompiler), Order = 10)]
internal partial class DecompilerSettingsPanel : UserControl, IOptionPage internal partial class DecompilerSettingsPanel : IOptionPage
{ {
public DecompilerSettingsPanel() public DecompilerSettingsPanel()
{ {
@ -59,58 +53,9 @@ namespace ICSharpCode.ILSpy.Options
MainWindow.Instance.AssemblyListManager.UseDebugSymbols = newSettings.UseDebugSymbols; MainWindow.Instance.AssemblyListManager.UseDebugSymbols = newSettings.UseDebugSymbols;
} }
private void OnGroupChecked(object sender, RoutedEventArgs e)
{
CheckGroup((CollectionViewGroup)((CheckBox)sender).DataContext, true);
}
private void OnGroupUnchecked(object sender, RoutedEventArgs e)
{
CheckGroup((CollectionViewGroup)((CheckBox)sender).DataContext, false);
}
void CheckGroup(CollectionViewGroup group, bool value)
{
foreach (var item in group.Items)
{
switch (item)
{
case CollectionViewGroup subGroup:
CheckGroup(subGroup, value);
break;
case CSharpDecompilerSetting setting:
setting.IsEnabled = value;
break;
}
}
}
bool IsGroupChecked(CollectionViewGroup group)
{
bool value = true;
foreach (var item in group.Items)
{
switch (item)
{
case CollectionViewGroup subGroup:
value = value && IsGroupChecked(subGroup);
break;
case CSharpDecompilerSetting setting:
value = value && setting.IsEnabled;
break;
}
}
return value;
}
private void OnGroupLoaded(object sender, RoutedEventArgs e)
{
CheckBox checkBox = (CheckBox)sender;
checkBox.IsChecked = IsGroupChecked((CollectionViewGroup)checkBox.DataContext);
}
public void LoadDefaults() public void LoadDefaults()
{ {
MainWindow.Instance.CurrentDecompilerSettings = new Decompiler.DecompilerSettings(); MainWindow.Instance.CurrentDecompilerSettings = new();
this.DataContext = new DecompilerSettingsViewModel(MainWindow.Instance.CurrentDecompilerSettings); this.DataContext = new DecompilerSettingsViewModel(MainWindow.Instance.CurrentDecompilerSettings);
} }
} }

101
ILSpy/Options/DecompilerSettingsViewModel.cs

@ -16,83 +16,120 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpy.TreeNodes;
using TomsToolbox.Wpf;
namespace ICSharpCode.ILSpy.Options namespace ICSharpCode.ILSpy.Options
{ {
public class DecompilerSettingsViewModel : INotifyPropertyChanged public sealed class DecompilerSettingsViewModel : ObservableObjectBase
{ {
public CSharpDecompilerSetting[] Settings { get; set; } public DecompilerSettingsGroupViewModel[] Settings { get; }
public DecompilerSettingsViewModel(Decompiler.DecompilerSettings settings) public DecompilerSettingsViewModel(Decompiler.DecompilerSettings settings)
{ {
Settings = typeof(Decompiler.DecompilerSettings).GetProperties() Settings = typeof(Decompiler.DecompilerSettings).GetProperties()
.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false) .Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
.Select(p => new CSharpDecompilerSetting(p) { IsEnabled = (bool)p.GetValue(settings) }) .Select(p => new DecompilerSettingsItemViewModel(p) { IsEnabled = p.GetValue(settings) is true })
.OrderBy(item => item.Category, NaturalStringComparer.Instance) .OrderBy(item => item.Category, NaturalStringComparer.Instance)
.ThenBy(item => item.Description) .GroupBy(p => p.Category)
.Select(g => new DecompilerSettingsGroupViewModel(g.Key, g.OrderBy(i => i.Description).ToArray()))
.ToArray(); .ToArray();
} }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Decompiler.DecompilerSettings ToDecompilerSettings() public Decompiler.DecompilerSettings ToDecompilerSettings()
{ {
var settings = new Decompiler.DecompilerSettings(); var settings = new Decompiler.DecompilerSettings();
foreach (var item in Settings)
foreach (var item in Settings.SelectMany(group => group.Settings))
{ {
item.Property.SetValue(settings, item.IsEnabled); item.Property.SetValue(settings, item.IsEnabled);
} }
return settings; return settings;
} }
} }
public class CSharpDecompilerSetting : INotifyPropertyChanged
public sealed class DecompilerSettingsGroupViewModel : ObservableObjectBase
{ {
bool isEnabled; private bool? _areAllItemsChecked;
public CSharpDecompilerSetting(PropertyInfo p) public DecompilerSettingsGroupViewModel(string category, DecompilerSettingsItemViewModel[] settings)
{ {
this.Property = p; Settings = settings;
this.Category = GetResourceString(p.GetCustomAttribute<CategoryAttribute>()?.Category ?? Resources.Other); Category = category;
this.Description = GetResourceString(p.GetCustomAttribute<DescriptionAttribute>()?.Description ?? p.Name);
}
public PropertyInfo Property { get; } _areAllItemsChecked = GetAreAllItemsChecked(Settings);
public bool IsEnabled { foreach (DecompilerSettingsItemViewModel viewModel in settings)
get => isEnabled; {
viewModel.PropertyChanged += Item_PropertyChanged;
}
}
public bool? AreAllItemsChecked {
get => _areAllItemsChecked;
set { set {
if (value != isEnabled) SetProperty(ref _areAllItemsChecked, value);
if (!value.HasValue)
return;
foreach (var setting in Settings)
{ {
isEnabled = value; setting.IsEnabled = value.Value;
OnPropertyChanged();
} }
} }
} }
public string Description { get; set; } public string Category { get; }
public string Category { get; set; } public DecompilerSettingsItemViewModel[] Settings { get; }
public event PropertyChangedEventHandler PropertyChanged; private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(DecompilerSettingsItemViewModel.IsEnabled))
{
AreAllItemsChecked = GetAreAllItemsChecked(Settings);
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) private static bool? GetAreAllItemsChecked(ICollection<DecompilerSettingsItemViewModel> settings)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); var numberOfEnabledItems = settings.Count(item => item.IsEnabled);
if (numberOfEnabledItems == settings.Count)
return true;
if (numberOfEnabledItems == 0)
return false;
return null;
}
}
public sealed class DecompilerSettingsItemViewModel(PropertyInfo property) : ObservableObjectBase
{
private bool _isEnabled;
public PropertyInfo Property { get; } = property;
public bool IsEnabled {
get => _isEnabled;
set => SetProperty(ref _isEnabled, value);
} }
static string GetResourceString(string key) public string Description { get; set; } = GetResourceString(property.GetCustomAttribute<DescriptionAttribute>()?.Description ?? property.Name);
public string Category { get; set; } = GetResourceString(property.GetCustomAttribute<CategoryAttribute>()?.Category ?? Resources.Other);
private static string GetResourceString(string key)
{ {
var str = !string.IsNullOrEmpty(key) ? Resources.ResourceManager.GetString(key) : null; var str = !string.IsNullOrEmpty(key) ? Resources.ResourceManager.GetString(key) : null;
return string.IsNullOrEmpty(key) || string.IsNullOrEmpty(str) ? key : str; return string.IsNullOrEmpty(key) || string.IsNullOrEmpty(str) ? key : str;

Loading…
Cancel
Save