Refactored settings views

Improved global exception handling
Added plugins page
This commit is contained in:
Mark van Renswoude 2021-03-09 18:42:49 +01:00
parent b1b8498456
commit 4974e57221
51 changed files with 1077 additions and 488 deletions

View File

@ -1,8 +1,11 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
using System.Windows;
using Hardcodet.Wpf.TaskbarNotification;
using MassiveKnob.View;
using Serilog;
using SimpleInjector;
using WpfBindingErrors;
namespace MassiveKnob
{
@ -28,6 +31,9 @@ namespace MassiveKnob
{
base.OnStartup(e);
// Do not let WPF swallow exceptions in bindings
BindingExceptionThrower.Attach();
notifyIcon = (TaskbarIcon)FindResource("NotifyIcon");
Debug.Assert(notifyIcon != null, nameof(notifyIcon) + " != null");
}

View File

@ -0,0 +1,48 @@
using MassiveKnob.View;
using MassiveKnob.View.Settings;
using MassiveKnob.ViewModel;
using MassiveKnob.ViewModel.Settings;
using SimpleInjector;
namespace MassiveKnob
{
public static class ContainerBuilder
{
public static Container Create()
{
var container = new Container();
container.Options.EnableAutoVerification = false;
container.Register<App>();
container.Register<SettingsWindow>();
container.Register<SettingsViewModel>();
container.Register<SettingsDeviceView>();
container.Register<SettingsDeviceViewModel>();
container.Register<SettingsAnalogInputsView>();
container.Register<SettingsAnalogInputsViewModel>();
container.Register<SettingsDigitalInputsView>();
container.Register<SettingsDigitalInputsViewModel>();
container.Register<SettingsAnalogOutputsView>();
container.Register<SettingsAnalogOutputsViewModel>();
container.Register<SettingsDigitalOutputsView>();
container.Register<SettingsDigitalOutputsViewModel>();
container.Register<SettingsLoggingView>();
container.Register<SettingsLoggingViewModel>();
container.Register<SettingsStartupView>();
container.Register<SettingsStartupViewModel>();
container.Register<SettingsPluginsView>();
container.Register<SettingsPluginsViewModel>();
return container;
}
}
}

View File

@ -5,7 +5,16 @@ namespace MassiveKnob.Core
{
public interface IPluginManager
{
IEnumerable<IMassiveKnobPluginInfo> GetPlugins();
IEnumerable<IMassiveKnobDevicePlugin> GetDevicePlugins();
IEnumerable<IMassiveKnobActionPlugin> GetActionPlugins();
}
public interface IMassiveKnobPluginInfo
{
string Filename { get; }
IMassiveKnobPlugin Plugin { get; }
}
}

View File

@ -34,23 +34,28 @@ namespace MassiveKnob.Core
public class PluginManager : IPluginManager
{
private readonly ILogger logger;
private readonly List<IMassiveKnobPlugin> plugins = new List<IMassiveKnobPlugin>();
private readonly List<IMassiveKnobPluginInfo> plugins = new List<IMassiveKnobPluginInfo>();
public PluginManager(ILogger logger)
{
this.logger = logger;
}
public IEnumerable<IMassiveKnobPluginInfo> GetPlugins()
{
return plugins;
}
public IEnumerable<IMassiveKnobDevicePlugin> GetDevicePlugins()
{
return plugins.Where(p => p is IMassiveKnobDevicePlugin).Cast<IMassiveKnobDevicePlugin>();
return plugins.Where(p => p.Plugin is IMassiveKnobDevicePlugin).Select(p => (IMassiveKnobDevicePlugin)p.Plugin);
}
public IEnumerable<IMassiveKnobActionPlugin> GetActionPlugins()
{
return plugins.Where(p => p is IMassiveKnobActionPlugin).Cast<IMassiveKnobActionPlugin>();
return plugins.Where(p => p.Plugin is IMassiveKnobActionPlugin).Select(p => (IMassiveKnobActionPlugin)p.Plugin);
}
@ -190,7 +195,7 @@ namespace MassiveKnob.Core
logger.Information("Found plugin with Id {pluginId}: {name}", plugin.PluginId, plugin.Name);
ValidateRegistration(filename, plugin, registeredIds);
plugins.Add((IMassiveKnobPlugin)pluginInstance);
plugins.Add(new PluginInfo(filename, (IMassiveKnobPlugin)pluginInstance));
}
}
@ -280,5 +285,19 @@ namespace MassiveKnob.Core
// ReSharper disable once UnusedAutoPropertyAccessor.Local - for JSON deserialization
public string EntryAssembly { get; set; }
}
private class PluginInfo : IMassiveKnobPluginInfo
{
public string Filename { get; }
public IMassiveKnobPlugin Plugin { get; }
public PluginInfo(string filename, IMassiveKnobPlugin plugin)
{
Filename = filename;
Plugin = plugin;
}
}
}
}

View File

@ -58,6 +58,7 @@
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="ContainerBuilder.cs" />
<Compile Include="Helpers\ComboBoxTemplateSelector.cs" />
<Compile Include="Helpers\ComparisonConverter.cs" />
<Compile Include="Helpers\SerialQueue.cs" />
@ -72,10 +73,17 @@
<Compile Include="ViewModel\DeviceViewModel.cs" />
<Compile Include="ViewModel\InputOutputViewModel.cs" />
<Compile Include="ViewModel\MenuItemProperties.cs" />
<Compile Include="ViewModel\PluginViewModel.cs" />
<Compile Include="ViewModel\SettingsViewModel.cs" />
<Compile Include="View\InputOutputView.xaml.cs">
<DependentUpon>InputOutputView.xaml</DependentUpon>
</Compile>
<Compile Include="ViewModel\Settings\SettingsAnalogOutputsViewModel.cs" />
<Compile Include="ViewModel\Settings\SettingsDigitalOutputsViewModel.cs" />
<Compile Include="ViewModel\Settings\SettingsDigitalInputsViewModel.cs" />
<Compile Include="ViewModel\Settings\SettingsAnalogInputsViewModel.cs" />
<Compile Include="ViewModel\Settings\SettingsDeviceViewModel.cs" />
<Compile Include="ViewModel\Settings\BaseSettingsInputOutputViewModel.cs" />
<Compile Include="ViewModel\Settings\SettingsLoggingViewModel.cs" />
<Compile Include="ViewModel\Settings\SettingsPluginsViewModel.cs" />
<Compile Include="ViewModel\Settings\SettingsStartupViewModel.cs" />
<Compile Include="View\SettingsWindow.xaml.cs">
<DependentUpon>SettingsWindow.xaml</DependentUpon>
</Compile>
@ -88,26 +96,24 @@
<DesignTime>True</DesignTime>
<DependentUpon>Strings.resx</DependentUpon>
</Compile>
<Compile Include="View\Settings\AnalogInputsView.xaml.cs">
<DependentUpon>AnalogInputsView.xaml</DependentUpon>
<Compile Include="View\Settings\BaseSettingsInputOutputView.xaml.cs">
<DependentUpon>BaseSettingsInputOutputView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\AnalogOutputsView.xaml.cs">
<DependentUpon>AnalogOutputsView.xaml</DependentUpon>
<Compile Include="View\Settings\SettingsAnalogOutputsView.cs" />
<Compile Include="View\Settings\SettingsDigitalOutputsView.cs" />
<Compile Include="View\Settings\SettingsDigitalInputsView.cs" />
<Compile Include="View\Settings\SettingsAnalogInputsView.cs" />
<Compile Include="View\Settings\SettingsPluginsView.xaml.cs">
<DependentUpon>SettingsPluginsView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\StartupView.xaml.cs">
<DependentUpon>StartupView.xaml</DependentUpon>
<Compile Include="View\Settings\SettingsStartupView.xaml.cs">
<DependentUpon>SettingsStartupView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\LoggingView.xaml.cs">
<DependentUpon>LoggingView.xaml</DependentUpon>
<Compile Include="View\Settings\SettingsLoggingView.xaml.cs">
<DependentUpon>SettingsLoggingView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\DigitalInputsView.xaml.cs">
<DependentUpon>DigitalInputsView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\DigitalOutputsView.xaml.cs">
<DependentUpon>DigitalOutputsView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\DeviceView.xaml.cs">
<DependentUpon>DeviceView.xaml</DependentUpon>
<Compile Include="View\Settings\SettingsDeviceView.xaml.cs">
<DependentUpon>SettingsDeviceView.xaml</DependentUpon>
</Compile>
<EmbeddedResource Include="Strings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
@ -158,6 +164,9 @@
<PackageReference Include="System.Reactive">
<Version>5.0.0</Version>
</PackageReference>
<PackageReference Include="WpfBindingErrors">
<Version>1.1.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\MainIcon.ico" />
@ -192,35 +201,31 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Plugins.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Startup.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="View\Settings\AnalogInputsView.xaml">
<Page Include="View\Settings\BaseSettingsInputOutputView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\Settings\AnalogOutputsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\Settings\StartupView.xaml">
<Page Include="View\Settings\SettingsPluginsView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="View\Settings\LoggingView.xaml">
<Page Include="View\Settings\SettingsStartupView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="View\Settings\DigitalInputsView.xaml">
<SubType>Designer</SubType>
<Page Include="View\Settings\SettingsLoggingView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\Settings\DigitalOutputsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\Settings\DeviceView.xaml">
<Page Include="View\Settings\SettingsDeviceView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
@ -230,10 +235,6 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\InputOutputView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\SettingsWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View File

@ -4,10 +4,7 @@ using System.Text;
using System.Windows;
using MassiveKnob.Core;
using MassiveKnob.Settings;
using MassiveKnob.View;
using MassiveKnob.ViewModel;
using Serilog;
using SimpleInjector;
namespace MassiveKnob
{
@ -19,23 +16,40 @@ namespace MassiveKnob
{
var settings = MassiveKnobSettingsJsonSerializer.Deserialize();
var loggingSwitch = new LoggingSwitch();
loggingSwitch.SetLogging(settings.Log.Enabled, settings.Log.Level);
var logFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Logs");
var logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Filter.ByIncludingOnly(loggingSwitch.IsIncluded)
.Enrich.FromLogContext()
.WriteTo.File(
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Logs", @".log"),
Path.Combine(logFilePath, @".log"),
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{@Context}{NewLine}{Exception}")
.CreateLogger();
logger.Information("MassiveKnob starting");
var pluginManager = new PluginManager(logger);
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
var e = (Exception)args.ExceptionObject;
logger.Error(e, "Unhandled exception: {message}", e.Message);
MessageBox.Show(
"Oops, something went very wrong. Please notify the developer and include this message, you can copy it using Ctrl-C. " +
"Preferably also include the log file which can be found at:" + Environment.NewLine + logFilePath +
Environment.NewLine + Environment.NewLine +
e.Message, "Massive Knob - Fatal error", MessageBoxButton.OK, MessageBoxImage.Error);
};
var pluginManager = new PluginManager(logger);
var messages = new StringBuilder();
pluginManager.Load((exception, filename) =>
{
@ -48,23 +62,17 @@ namespace MassiveKnob
return 1;
}
var orchestrator = new MassiveKnobOrchestrator(pluginManager, logger, settings);
orchestrator.Load();
var container = new Container();
container.Options.EnableAutoVerification = false;
container.RegisterInstance(logger);
var container = ContainerBuilder.Create();
container.RegisterInstance<ILogger>(logger);
container.RegisterInstance<ILoggingSwitch>(loggingSwitch);
container.RegisterInstance<IPluginManager>(pluginManager);
container.RegisterInstance<IMassiveKnobOrchestrator>(orchestrator);
container.Register<App>();
container.Register<SettingsWindow>();
container.Register<SettingsViewModel>();
var app = container.GetInstance<App>();
app.Run();

View File

@ -0,0 +1,17 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Viewbox Stretch="Uniform" x:Key="Plugins" x:Shared="False">
<Canvas Width="24" Height="24">
<Canvas.Resources>
<ResourceDictionary Source="IconStyle.xaml" />
</Canvas.Resources>
<Polygon Points="12 2 2 7 12 12 22 7 12 2" FillRule="NonZero" Style="{StaticResource IconStroke}" />
<Polyline Points="2 17 12 22 22 17" FillRule="NonZero" Style="{StaticResource IconStroke}" />
<Polyline Points="2 12 12 17 22 12" FillRule="NonZero" Style="{StaticResource IconStroke}" />
</Canvas>
</Viewbox>
</ResourceDictionary>

View File

@ -15,7 +15,8 @@ namespace MassiveKnob.Settings
AnalogOutputs,
DigitalOutputs,
Logging,
Startup
Startup,
Plugins
}

View File

@ -303,6 +303,15 @@ namespace MassiveKnob {
}
}
/// <summary>
/// Looks up a localized string similar to Plugins.
/// </summary>
public static string MenuItemPlugins {
get {
return ResourceManager.GetString("MenuItemPlugins", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Startup.
/// </summary>

View File

@ -198,6 +198,9 @@
<data name="MenuItemLogging" xml:space="preserve">
<value>Logging</value>
</data>
<data name="MenuItemPlugins" xml:space="preserve">
<value>Plugins</value>
</data>
<data name="MenuItemStartup" xml:space="preserve">
<value>Startup</value>
</data>

View File

@ -22,8 +22,8 @@
<Setter Property="Margin" Value="0,0,0,4" />
</Style>
<Style TargetType="StackPanel" x:Key="Content">
<Setter Property="Margin" Value="8" />
<Style x:Key="Content">
<Setter Property="Control.Margin" Value="8" />
</Style>
<Style TargetType="ContentControl" x:Key="SettingsControl">
@ -41,4 +41,19 @@
<Style TargetType="TextBlock" x:Key="SubLabel">
<Setter Property="Foreground" Value="#808080" />
</Style>
<Style TargetType="TextBlock" x:Key="PluginName">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
<Style TargetType="TextBlock" x:Key="PluginDescription">
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="Margin" Value="0,0,0,8" />
</Style>
<Style TargetType="TextBlock" x:Key="PluginFilename">
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="Foreground" Value="#808080" />
</Style>
</ResourceDictionary>

View File

@ -1,65 +0,0 @@
<UserControl x:Class="MassiveKnob.View.InputOutputView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:massiveKnob="clr-namespace:MassiveKnob"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="600"
d:DataContext="{d:DesignInstance viewModel:InputOutputViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Style.xaml"></ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
<DataTemplate x:Key="ActionDropdownItem">
<StackPanel Orientation="Vertical" d:DataContext="{d:DesignInstance Type=viewModel:ActionViewModel}">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" Style="{StaticResource ComboBoxDescription}" Visibility="{Binding DescriptionVisibility}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="ActionSelectedItem">
<TextBlock Text="{Binding Name}" d:DataContext="{d:DesignInstance Type=viewModel:ActionViewModel}" />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" Style="{StaticResource Content}">
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource SubHeader}"></TextBlock>
<StackPanel Orientation="Vertical">
<ComboBox
ItemsSource="{Binding Actions}"
SelectedItem="{Binding SelectedAction}"
IsSynchronizedWithCurrentItem="False"
ItemTemplateSelector="{helpers:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource ActionSelectedItem},
DropdownItemsTemplate={StaticResource ActionDropdownItem}}" />
<ContentControl Focusable="False" Content="{Binding ActionSettingsControl}" Style="{StaticResource SettingsControl}" />
<Grid Margin="0,24,0,0" Visibility="{Binding DigitalToAnalogVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Text="{x:Static massiveKnob:Strings.DigitalToAnalogDescription}" TextWrapping="Wrap" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,8,8,8" VerticalAlignment="Center" Text="{x:Static massiveKnob:Strings.DigitalToAnalogOn}" />
<Slider Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Value="{Binding DigitalToAnalogOn}" Minimum="0" Maximum="100" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,8,8,8" VerticalAlignment="Center" Text="{x:Static massiveKnob:Strings.DigitalToAnalogOff}" />
<Slider Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" Value="{Binding DigitalToAnalogOff}" Minimum="0" Maximum="100" />
</Grid>
</StackPanel>
</StackPanel>
</UserControl>

View File

@ -1,13 +0,0 @@
namespace MassiveKnob.View
{
/// <summary>
/// Interaction logic for InputOutputView.xaml
/// </summary>
public partial class InputOutputView
{
public InputOutputView()
{
InitializeComponent();
}
}
}

View File

@ -1,29 +0,0 @@
<UserControl x:Class="MassiveKnob.View.Settings.AnalogInputsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:view="clr-namespace:MassiveKnob.View"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<ItemsControl ItemsSource="{Binding AnalogInputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View File

@ -1,13 +0,0 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for AnalogInputsView.xaml
/// </summary>
public partial class AnalogInputsView
{
public AnalogInputsView()
{
InitializeComponent();
}
}
}

View File

@ -1,28 +0,0 @@
<UserControl x:Class="MassiveKnob.View.Settings.AnalogOutputsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:view="clr-namespace:MassiveKnob.View"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<ItemsControl ItemsSource="{Binding AnalogOutputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View File

@ -1,13 +0,0 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for AnalogOutputsView.xaml
/// </summary>
public partial class AnalogOutputsView
{
public AnalogOutputsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,75 @@
<UserControl x:Class="MassiveKnob.View.Settings.BaseSettingsInputOutputView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:settings="clr-namespace:MassiveKnob.ViewModel.Settings"
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:massiveKnob="clr-namespace:MassiveKnob"
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=settings:BaseSettingsInputOutputViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
<DataTemplate x:Key="ActionDropdownItem">
<StackPanel Orientation="Vertical" d:DataContext="{d:DesignInstance Type=viewModel:ActionViewModel}">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" Style="{StaticResource ComboBoxDescription}" Visibility="{Binding DescriptionVisibility}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="ActionSelectedItem">
<TextBlock Text="{Binding Name}" d:DataContext="{d:DesignInstance Type=viewModel:ActionViewModel}" />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<ItemsControl ItemsSource="{Binding InputOutputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Style="{StaticResource Content}">
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource SubHeader}"></TextBlock>
<StackPanel Orientation="Vertical">
<ComboBox
ItemsSource="{Binding Actions}"
SelectedItem="{Binding SelectedAction}"
IsSynchronizedWithCurrentItem="False"
ItemTemplateSelector="{helpers:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource ActionSelectedItem},
DropdownItemsTemplate={StaticResource ActionDropdownItem}}" />
<ContentControl Focusable="False" Content="{Binding ActionSettingsControl}" Style="{StaticResource SettingsControl}" />
<Grid Margin="0,24,0,0" Visibility="{Binding DigitalToAnalogVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Text="{x:Static massiveKnob:Strings.DigitalToAnalogDescription}" TextWrapping="Wrap" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,8,8,8" VerticalAlignment="Center" Text="{x:Static massiveKnob:Strings.DigitalToAnalogOn}" />
<Slider Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Value="{Binding DigitalToAnalogOn}" Minimum="0" Maximum="100" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,8,8,8" VerticalAlignment="Center" Text="{x:Static massiveKnob:Strings.DigitalToAnalogOff}" />
<Slider Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" Value="{Binding DigitalToAnalogOff}" Minimum="0" Maximum="100" />
</Grid>
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,16 @@
using MassiveKnob.ViewModel.Settings;
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for BaseSettingsInputOutputView.xaml
/// </summary>
public partial class BaseSettingsInputOutputView
{
public BaseSettingsInputOutputView(BaseSettingsInputOutputViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -1,13 +0,0 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for DeviceView.xaml
/// </summary>
public partial class DeviceView
{
public DeviceView()
{
InitializeComponent();
}
}
}

View File

@ -1,28 +0,0 @@
<UserControl x:Class="MassiveKnob.View.Settings.DigitalInputsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:view="clr-namespace:MassiveKnob.View"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<ItemsControl ItemsSource="{Binding DigitalInputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View File

@ -1,13 +0,0 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for DigitalInputsView.xaml
/// </summary>
public partial class DigitalInputsView
{
public DigitalInputsView()
{
InitializeComponent();
}
}
}

View File

@ -1,28 +0,0 @@
<UserControl x:Class="MassiveKnob.View.Settings.DigitalOutputsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:view="clr-namespace:MassiveKnob.View"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<ItemsControl ItemsSource="{Binding DigitalOutputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View File

@ -1,13 +0,0 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for DigitalOutputsView.xaml
/// </summary>
public partial class DigitalOutputsView
{
public DigitalOutputsView()
{
InitializeComponent();
}
}
}

View File

@ -1,13 +0,0 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for LoggingView.xaml
/// </summary>
public partial class LoggingView
{
public LoggingView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,12 @@
using MassiveKnob.ViewModel.Settings;
namespace MassiveKnob.View.Settings
{
public class SettingsAnalogInputsView : BaseSettingsInputOutputView
{
// ReSharper disable once SuggestBaseTypeForParameter - required for injection
public SettingsAnalogInputsView(SettingsAnalogInputsViewModel viewModel) : base(viewModel)
{
}
}
}

View File

@ -0,0 +1,12 @@
using MassiveKnob.ViewModel.Settings;
namespace MassiveKnob.View.Settings
{
public class SettingsAnalogOutputsView : BaseSettingsInputOutputView
{
// ReSharper disable once SuggestBaseTypeForParameter - required for injection
public SettingsAnalogOutputsView(SettingsAnalogOutputsViewModel viewModel) : base(viewModel)
{
}
}
}

View File

@ -1,13 +1,14 @@
<UserControl x:Class="MassiveKnob.View.Settings.DeviceView"
<UserControl x:Class="MassiveKnob.View.Settings.SettingsDeviceView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
xmlns:settings="clr-namespace:MassiveKnob.ViewModel.Settings"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
d:DataContext="{d:DesignInstance Type=settings:SettingsDeviceViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>

View File

@ -0,0 +1,16 @@
using MassiveKnob.ViewModel.Settings;
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for SettingsDeviceView.xaml
/// </summary>
public partial class SettingsDeviceView
{
public SettingsDeviceView(SettingsDeviceViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,12 @@
using MassiveKnob.ViewModel.Settings;
namespace MassiveKnob.View.Settings
{
public class SettingsDigitalInputsView : BaseSettingsInputOutputView
{
// ReSharper disable once SuggestBaseTypeForParameter - required for injection
public SettingsDigitalInputsView(SettingsDigitalInputsViewModel viewModel) : base(viewModel)
{
}
}
}

View File

@ -0,0 +1,12 @@
using MassiveKnob.ViewModel.Settings;
namespace MassiveKnob.View.Settings
{
public class SettingsDigitalOutputsView : BaseSettingsInputOutputView
{
// ReSharper disable once SuggestBaseTypeForParameter - required for injection
public SettingsDigitalOutputsView(SettingsDigitalOutputsViewModel viewModel) : base(viewModel)
{
}
}
}

View File

@ -1,4 +1,4 @@
<UserControl x:Class="MassiveKnob.View.Settings.LoggingView"
<UserControl x:Class="MassiveKnob.View.Settings.SettingsLoggingView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

View File

@ -0,0 +1,16 @@
using MassiveKnob.ViewModel.Settings;
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for SettingsLoggingView.xaml
/// </summary>
public partial class SettingsLoggingView
{
public SettingsLoggingView(SettingsLoggingViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,29 @@
<UserControl x:Class="MassiveKnob.View.Settings.SettingsPluginsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:settings="clr-namespace:MassiveKnob.ViewModel.Settings"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=settings:SettingsPluginsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<ItemsControl ItemsSource="{Binding Plugins}" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display" Style="{StaticResource Content}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Margin="0,0,0,24">
<TextBlock Text="{Binding Name}" Style="{StaticResource PluginName}" />
<TextBlock Text="{Binding Description}" Visibility="{Binding DescriptionVisibility}" Style="{StaticResource PluginDescription}" />
<TextBlock Text="{Binding Filename}" Style="{StaticResource PluginFilename}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>

View File

@ -0,0 +1,16 @@
using MassiveKnob.ViewModel.Settings;
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for SettingsPluginsView.xaml
/// </summary>
public partial class SettingsPluginsView
{
public SettingsPluginsView(SettingsPluginsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -1,4 +1,4 @@
<UserControl x:Class="MassiveKnob.View.Settings.StartupView"
<UserControl x:Class="MassiveKnob.View.Settings.SettingsStartupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

View File

@ -0,0 +1,16 @@
using MassiveKnob.ViewModel.Settings;
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for SettingsStartupView.xaml
/// </summary>
public partial class SettingsStartupView
{
public SettingsStartupView(SettingsStartupViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -1,13 +0,0 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for StartupView.xaml
/// </summary>
public partial class StartupView
{
public StartupView()
{
InitializeComponent();
}
}
}

View File

@ -23,6 +23,7 @@
<ResourceDictionary Source="../Resources/Logging.xaml" />
<ResourceDictionary Source="../Resources/Device.xaml" />
<ResourceDictionary Source="../Resources/Startup.xaml" />
<ResourceDictionary Source="../Resources/Plugins.xaml" />
</ResourceDictionary.MergedDictionaries>
<helpers:ComparisonConverter x:Key="ComparisonConverter" />
@ -81,6 +82,7 @@
<TextBlock Style="{StaticResource MenuGroup}" Text="{x:Static massiveKnob:Strings.MenuGroupSettings}" />
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemLogging}" viewModel:MenuItemProperties.Icon="{StaticResource Logging}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.Logging}}"/>
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemStartup}" viewModel:MenuItemProperties.Icon="{StaticResource Startup}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.Startup}}"/>
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemPlugins}" viewModel:MenuItemProperties.Icon="{StaticResource Plugins}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.Plugins}}"/>
</StackPanel>
</ScrollViewer>

View File

@ -125,7 +125,7 @@ namespace MassiveKnob.ViewModel
// ReSharper restore UnusedMember.Global
public InputOutputViewModel(SettingsViewModel settingsViewModel, IMassiveKnobOrchestrator orchestrator,
public InputOutputViewModel(IEnumerable<ActionViewModel> allActions, IMassiveKnobOrchestrator orchestrator,
MassiveKnobActionType actionType, int index)
{
this.orchestrator = orchestrator;
@ -155,7 +155,7 @@ namespace MassiveKnob.ViewModel
}
Actions = settingsViewModel.Actions.Where(AllowAction).ToList();
Actions = allActions.Where(AllowAction).ToList();
var actionInfo = orchestrator.GetAction(actionType, index);

View File

@ -0,0 +1,23 @@
using System.Windows;
namespace MassiveKnob.ViewModel
{
public class PluginViewModel
{
// ReSharper disable UnusedMember.Global - used by WPF Binding
public string Name { get; }
public string Description { get; }
public string Filename { get; }
public Visibility DescriptionVisibility => string.IsNullOrEmpty(Description) ? Visibility.Collapsed : Visibility.Visible;
// ReSharper restore UnusedMember.Global
public PluginViewModel(string name, string description, string filename)
{
Name = name;
Description = description;
Filename = filename;
}
}
}

View File

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using MassiveKnob.Core;
using MassiveKnob.Plugin;
namespace MassiveKnob.ViewModel.Settings
{
public class BaseSettingsInputOutputViewModel : IDisposable, INotifyPropertyChanged
{
private readonly IMassiveKnobOrchestrator orchestrator;
private readonly MassiveKnobActionType inputOutputType;
private DeviceSpecs? specs;
private readonly IDisposable activeDeviceSubscription;
private IEnumerable<InputOutputViewModel> inputOutputs;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public IList<ActionViewModel> Actions { get; }
public DeviceSpecs? Specs
{
get => specs;
set
{
specs = value;
OnPropertyChanged();
DisposeInputOutputViewModels();
int inputOutputCount;
switch (inputOutputType)
{
case MassiveKnobActionType.InputAnalog:
inputOutputCount = specs?.AnalogInputCount ?? 0;
break;
case MassiveKnobActionType.InputDigital:
inputOutputCount = specs?.DigitalInputCount ?? 0;
break;
case MassiveKnobActionType.OutputAnalog:
inputOutputCount = specs?.AnalogOutputCount ?? 0;
break;
case MassiveKnobActionType.OutputDigital:
inputOutputCount = specs?.DigitalOutputCount ?? 0;
break;
default:
throw new ArgumentOutOfRangeException();
}
InputOutputs = Enumerable
.Range(0, inputOutputCount)
.Select(i => new InputOutputViewModel(Actions, orchestrator, inputOutputType, i))
.ToList();
}
}
public IEnumerable<InputOutputViewModel> InputOutputs
{
get => inputOutputs;
set
{
inputOutputs = value;
OnPropertyChanged();
}
}
// ReSharper restore UnusedMember.Global
public BaseSettingsInputOutputViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator, MassiveKnobActionType inputOutputType)
{
this.orchestrator = orchestrator;
this.inputOutputType = inputOutputType;
// For design-time support
if (orchestrator == null)
return;
var allActions = new List<ActionViewModel>
{
new ActionViewModel(null, null)
};
allActions.AddRange(
pluginManager.GetActionPlugins()
.SelectMany(ap => ap.Actions.Select(a => new ActionViewModel(ap, a)))
.OrderBy(a => a.Name.ToLower()));
Actions = allActions;
activeDeviceSubscription = orchestrator.ActiveDeviceSubject.Subscribe(info =>
{
Application.Current?.Dispatcher.Invoke(() =>
{
Specs = info.Specs;
});
});
if (orchestrator.ActiveDevice != null)
Specs = orchestrator.ActiveDevice.Specs;
}
public void Dispose()
{
DisposeInputOutputViewModels();
activeDeviceSubscription?.Dispose();
}
private void DisposeInputOutputViewModels()
{
if (inputOutputs == null)
return;
foreach (var viewModel in inputOutputs)
viewModel.Dispose();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class BaseSettingsInputOutputViewModelDesignTime : BaseSettingsInputOutputViewModel
{
public BaseSettingsInputOutputViewModelDesignTime()
: base(null, null, MassiveKnobActionType.InputAnalog)
{
Specs = new DeviceSpecs(2, 2, 2, 2);
}
}
}

View File

@ -0,0 +1,13 @@
using MassiveKnob.Core;
using MassiveKnob.Plugin;
namespace MassiveKnob.ViewModel.Settings
{
public class SettingsAnalogInputsViewModel : BaseSettingsInputOutputViewModel
{
public SettingsAnalogInputsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator)
: base(pluginManager, orchestrator, MassiveKnobActionType.InputAnalog)
{
}
}
}

View File

@ -0,0 +1,13 @@
using MassiveKnob.Core;
using MassiveKnob.Plugin;
namespace MassiveKnob.ViewModel.Settings
{
public class SettingsAnalogOutputsViewModel : BaseSettingsInputOutputViewModel
{
public SettingsAnalogOutputsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator)
: base(pluginManager, orchestrator, MassiveKnobActionType.OutputAnalog)
{
}
}
}

View File

@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Controls;
using System.Windows.Media;
using MassiveKnob.Core;
namespace MassiveKnob.ViewModel.Settings
{
public class SettingsDeviceViewModel : IDisposable, INotifyPropertyChanged
{
private readonly IMassiveKnobOrchestrator orchestrator;
private DeviceViewModel selectedDevice;
private UserControl settingsControl;
private readonly IDisposable deviceStatusSubscription;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public IList<DeviceViewModel> Devices { get; }
public DeviceViewModel SelectedDevice
{
get => selectedDevice;
set
{
if (value == selectedDevice)
return;
selectedDevice = value;
var deviceInfo = orchestrator?.SetActiveDevice(value?.Device);
OnPropertyChanged();
SettingsControl = deviceInfo?.Instance.CreateSettingsControl();
}
}
public UserControl SettingsControl
{
get => settingsControl;
set
{
if (value == settingsControl)
return;
if (settingsControl is IDisposable disposable)
disposable.Dispose();
settingsControl = value;
OnPropertyChanged();
}
}
public string ConnectionStatusText
{
get
{
if (orchestrator == null)
return "Design-time";
switch (orchestrator.DeviceStatus)
{
case MassiveKnobDeviceStatus.Disconnected:
return Strings.DeviceStatusDisconnected;
case MassiveKnobDeviceStatus.Connecting:
return Strings.DeviceStatusConnecting;
case MassiveKnobDeviceStatus.Connected:
return Strings.DeviceStatusConnected;
default:
return null;
}
}
}
public Brush ConnectionStatusColor
{
get
{
if (orchestrator == null)
return Brushes.Fuchsia;
switch (orchestrator.DeviceStatus)
{
case MassiveKnobDeviceStatus.Disconnected:
return Brushes.DarkRed;
case MassiveKnobDeviceStatus.Connecting:
return Brushes.Orange;
case MassiveKnobDeviceStatus.Connected:
return Brushes.ForestGreen;
default:
return null;
}
}
}
// ReSharper restore UnusedMember.Global
public SettingsDeviceViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator)
{
this.orchestrator = orchestrator;
// For design-time support
if (orchestrator == null)
return;
deviceStatusSubscription = orchestrator.DeviceStatusSubject.Subscribe(status =>
{
OnDependantPropertyChanged(nameof(ConnectionStatusColor));
OnDependantPropertyChanged(nameof(ConnectionStatusText));
});
Devices = pluginManager.GetDevicePlugins()
.SelectMany(dp => dp.Devices.Select(d => new DeviceViewModel(dp, d)))
.OrderBy(d => d.Name.ToLower())
.ToList();
if (orchestrator.ActiveDevice == null)
return;
selectedDevice = Devices.Single(d => d.Device.DeviceId == orchestrator.ActiveDevice.Info.DeviceId);
SettingsControl = orchestrator.ActiveDevice.Instance.CreateSettingsControl();
}
public void Dispose()
{
if (SettingsControl is IDisposable disposable)
disposable.Dispose();
deviceStatusSubscription?.Dispose();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnDependantPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class SettingsDeviceViewModelDesignTime : SettingsDeviceViewModel
{
public SettingsDeviceViewModelDesignTime() : base(null, null)
{
}
}
}

View File

@ -0,0 +1,13 @@
using MassiveKnob.Core;
using MassiveKnob.Plugin;
namespace MassiveKnob.ViewModel.Settings
{
public class SettingsDigitalInputsViewModel : BaseSettingsInputOutputViewModel
{
public SettingsDigitalInputsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator)
: base(pluginManager, orchestrator, MassiveKnobActionType.InputDigital)
{
}
}
}

View File

@ -0,0 +1,13 @@
using MassiveKnob.Core;
using MassiveKnob.Plugin;
namespace MassiveKnob.ViewModel.Settings
{
public class SettingsDigitalOutputsViewModel : BaseSettingsInputOutputViewModel
{
public SettingsDigitalOutputsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator)
: base(pluginManager, orchestrator, MassiveKnobActionType.OutputDigital)
{
}
}
}

View File

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using MassiveKnob.Core;
using MassiveKnob.Settings;
using Serilog.Events;
namespace MassiveKnob.ViewModel.Settings
{
public class SettingsLoggingViewModel : INotifyPropertyChanged
{
private readonly IMassiveKnobOrchestrator orchestrator;
private readonly ILoggingSwitch loggingSwitch;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public IList<LoggingLevelViewModel> LoggingLevels { get; }
private LoggingLevelViewModel selectedLoggingLevel;
public LoggingLevelViewModel SelectedLoggingLevel
{
get => selectedLoggingLevel;
set
{
if (value == selectedLoggingLevel)
return;
selectedLoggingLevel = value;
OnPropertyChanged();
ApplyLoggingSettings();
}
}
private bool loggingEnabled;
public bool LoggingEnabled
{
get => loggingEnabled;
set
{
if (value == loggingEnabled)
return;
loggingEnabled = value;
OnPropertyChanged();
ApplyLoggingSettings();
}
}
// TODO (code quality) do not hardcode path here
public string LoggingOutputPath { get; } = string.Format(Strings.LoggingOutputPath,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob",
@"Logs"));
// ReSharper restore UnusedMember.Global
public SettingsLoggingViewModel(IMassiveKnobOrchestrator orchestrator, ILoggingSwitch loggingSwitch)
{
this.orchestrator = orchestrator;
this.loggingSwitch = loggingSwitch;
// For design-time support
if (orchestrator == null)
return;
var logSettings = orchestrator.GetSettings().Log;
LoggingLevels = new List<LoggingLevelViewModel>
{
new LoggingLevelViewModel(LogEventLevel.Error, Strings.LoggingLevelError, Strings.LoggingLevelErrorDescription),
new LoggingLevelViewModel(LogEventLevel.Warning, Strings.LoggingLevelWarning, Strings.LoggingLevelWarningDescription),
new LoggingLevelViewModel(LogEventLevel.Information, Strings.LoggingLevelInformation, Strings.LoggingLevelInformationDescription),
new LoggingLevelViewModel(LogEventLevel.Verbose, Strings.LoggingLevelVerbose, Strings.LoggingLevelVerboseDescription)
};
selectedLoggingLevel = LoggingLevels.SingleOrDefault(l => l.Level == logSettings.Level)
?? LoggingLevels.Single(l => l.Level == LogEventLevel.Information);
loggingEnabled = logSettings.Enabled;
}
private void ApplyLoggingSettings()
{
orchestrator?.UpdateSettings(settings =>
{
settings.Log.Enabled = LoggingEnabled;
settings.Log.Level = SelectedLoggingLevel.Level;
});
loggingSwitch?.SetLogging(LoggingEnabled, selectedLoggingLevel.Level);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MassiveKnob.Core;
namespace MassiveKnob.ViewModel.Settings
{
public class SettingsPluginsViewModel
{
// ReSharper disable UnusedMember.Global - used by WPF Binding
public IEnumerable<PluginViewModel> Plugins { get; protected set; }
// ReSharper restore UnusedMember.Global
public SettingsPluginsViewModel(IPluginManager pluginManager)
{
// Design-time support
if (pluginManager == null)
return;
Plugins = pluginManager.GetPlugins()
.Select(p => new PluginViewModel(p.Plugin.Name, p.Plugin.Description, p.Filename))
.OrderBy(p => p.Name, StringComparer.CurrentCultureIgnoreCase)
.ToList();
}
}
public class SettingsPluginsViewModelDesignTime : SettingsPluginsViewModel
{
public SettingsPluginsViewModelDesignTime()
: base(null)
{
Plugins = new[]
{
new PluginViewModel("Plugin without description", null, "D:\\Does\\Not\\Exist.dll"),
new PluginViewModel("Design-time plugin", "Fake plugin only visible at design-time.", "C:\\Does\\Not\\Exist.dll")
};
}
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Win32;
namespace MassiveKnob.ViewModel.Settings
{
public class SettingsStartupViewModel : INotifyPropertyChanged
{
public const string RunKey = @"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run";
public const string RunValue = @"MassiveKnob";
// ReSharper disable UnusedMember.Global - used by WPF Binding
private bool runAtStartup;
public bool RunAtStartup
{
get => runAtStartup;
set
{
if (value == runAtStartup)
return;
runAtStartup = value;
OnPropertyChanged();
ApplyRunAtStartup();
}
}
// ReSharper restore UnusedMember.Global
public SettingsStartupViewModel()
{
var runKey = Registry.CurrentUser.OpenSubKey(RunKey, false);
runAtStartup = runKey?.GetValue(RunValue) != null;
}
private void ApplyRunAtStartup()
{
var runKey = Registry.CurrentUser.OpenSubKey(RunKey, true);
Debug.Assert(runKey != null, nameof(runKey) + " != null");
if (RunAtStartup)
{
var entryAssembly = Assembly.GetEntryAssembly();
Debug.Assert(entryAssembly != null, nameof(entryAssembly) + " != null");
runKey.SetValue(RunValue, new Uri(entryAssembly.CodeBase).LocalPath);
}
else
{
runKey.DeleteValue(RunValue, false);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -1,55 +1,39 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using MassiveKnob.Core;
using MassiveKnob.Plugin;
using MassiveKnob.Settings;
using MassiveKnob.View.Settings;
using Microsoft.Win32;
using Serilog.Events;
namespace MassiveKnob.ViewModel
{
// TODO (code quality) split ViewModel for individual views, create viewmodel using container
// TODO (nice to have) installed plugins list
public class SettingsViewModel : IDisposable, INotifyPropertyChanged
{
private readonly Dictionary<SettingsMenuItem, Type> menuItemControls = new Dictionary<SettingsMenuItem, Type>
{
{ SettingsMenuItem.Device, typeof(DeviceView) },
{ SettingsMenuItem.AnalogInputs, typeof(AnalogInputsView) },
{ SettingsMenuItem.DigitalInputs, typeof(DigitalInputsView) },
{ SettingsMenuItem.AnalogOutputs, typeof(AnalogOutputsView) },
{ SettingsMenuItem.DigitalOutputs, typeof(DigitalOutputsView) },
{ SettingsMenuItem.Logging, typeof(LoggingView) },
{ SettingsMenuItem.Startup, typeof(StartupView) }
{ SettingsMenuItem.Device, typeof(SettingsDeviceView) },
{ SettingsMenuItem.AnalogInputs, typeof(SettingsAnalogInputsView) },
{ SettingsMenuItem.DigitalInputs, typeof(SettingsDigitalInputsView) },
{ SettingsMenuItem.AnalogOutputs, typeof(SettingsAnalogOutputsView) },
{ SettingsMenuItem.DigitalOutputs, typeof(SettingsDigitalOutputsView) },
{ SettingsMenuItem.Logging, typeof(SettingsLoggingView) },
{ SettingsMenuItem.Startup, typeof(SettingsStartupView) },
{ SettingsMenuItem.Plugins, typeof(SettingsPluginsView) }
};
private readonly SimpleInjector.Container container;
private readonly IMassiveKnobOrchestrator orchestrator;
private readonly ILoggingSwitch loggingSwitch;
private DeviceViewModel selectedDevice;
private UserControl selectedView;
private SettingsMenuItem selectedMenuItem;
private UserControl settingsControl;
private DeviceSpecs? specs;
private IEnumerable<InputOutputViewModel> analogInputs;
private IEnumerable<InputOutputViewModel> digitalInputs;
private IEnumerable<InputOutputViewModel> analogOutputs;
private IEnumerable<InputOutputViewModel> digitalOutputs;
private readonly IDisposable activeDeviceSubscription;
private readonly IDisposable deviceStatusSubscription;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public SettingsMenuItem SelectedMenuItem
@ -64,8 +48,8 @@ namespace MassiveKnob.ViewModel
OnPropertyChanged();
if (menuItemControls.TryGetValue(selectedMenuItem, out var viewType))
SelectedView = (UserControl) Activator.CreateInstance(viewType);
SelectedView = (UserControl)container?.GetInstance(viewType);
orchestrator?.UpdateSettings(settings =>
{
settings.UI.ActiveMenuItem = selectedMenuItem;
@ -88,42 +72,7 @@ namespace MassiveKnob.ViewModel
public IList<DeviceViewModel> Devices { get; }
public IList<ActionViewModel> Actions { get; }
public DeviceViewModel SelectedDevice
{
get => selectedDevice;
set
{
if (value == selectedDevice)
return;
selectedDevice = value;
var deviceInfo = orchestrator?.SetActiveDevice(value?.Device);
OnPropertyChanged();
SettingsControl = deviceInfo?.Instance.CreateSettingsControl();
}
}
public UserControl SettingsControl
{
get => settingsControl;
set
{
if (value == settingsControl)
return;
if (settingsControl is IDisposable disposable)
disposable.Dispose();
settingsControl = value;
OnPropertyChanged();
}
}
//public IList<ActionViewModel> Actions { get; }
public DeviceSpecs? Specs
{
@ -136,7 +85,7 @@ namespace MassiveKnob.ViewModel
OnDependantPropertyChanged("DigitalInputVisibility");
OnDependantPropertyChanged("AnalogOutputVisibility");
OnDependantPropertyChanged("DigitalOutputVisibility");
/*
DisposeInputOutputViewModels(AnalogInputs);
DisposeInputOutputViewModels(DigitalInputs);
DisposeInputOutputViewModels(AnalogOutputs);
@ -161,6 +110,7 @@ namespace MassiveKnob.ViewModel
.Range(0, specs?.DigitalOutputCount ?? 0)
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.OutputDigital, i))
.ToList();
*/
}
}
@ -169,6 +119,7 @@ namespace MassiveKnob.ViewModel
? Visibility.Visible
: Visibility.Collapsed;
/*
public IEnumerable<InputOutputViewModel> AnalogInputs
{
get => analogInputs;
@ -178,11 +129,13 @@ namespace MassiveKnob.ViewModel
OnPropertyChanged();
}
}
*/
public Visibility DigitalInputVisibility => specs.HasValue && specs.Value.DigitalInputCount > 0
? Visibility.Visible
: Visibility.Collapsed;
/*
public IEnumerable<InputOutputViewModel> DigitalInputs
{
get => digitalInputs;
@ -192,11 +145,13 @@ namespace MassiveKnob.ViewModel
OnPropertyChanged();
}
}
*/
public Visibility AnalogOutputVisibility => specs.HasValue && specs.Value.AnalogOutputCount > 0
? Visibility.Visible
: Visibility.Collapsed;
/*
public IEnumerable<InputOutputViewModel> AnalogOutputs
{
get => analogOutputs;
@ -206,11 +161,13 @@ namespace MassiveKnob.ViewModel
OnPropertyChanged();
}
}
*/
public Visibility DigitalOutputVisibility => specs.HasValue && specs.Value.DigitalOutputCount > 0
? Visibility.Visible
: Visibility.Collapsed;
/*
public IEnumerable<InputOutputViewModel> DigitalOutputs
{
get => digitalOutputs;
@ -221,7 +178,6 @@ namespace MassiveKnob.ViewModel
}
}
public IList<LoggingLevelViewModel> LoggingLevels { get; }
private LoggingLevelViewModel selectedLoggingLevel;
@ -281,62 +237,15 @@ namespace MassiveKnob.ViewModel
ApplyRunAtStartup();
}
}
public string ConnectionStatusText
{
get
{
if (orchestrator == null)
return "Design-time";
switch (orchestrator.DeviceStatus)
{
case MassiveKnobDeviceStatus.Disconnected:
return Strings.DeviceStatusDisconnected;
case MassiveKnobDeviceStatus.Connecting:
return Strings.DeviceStatusConnecting;
case MassiveKnobDeviceStatus.Connected:
return Strings.DeviceStatusConnected;
default:
return null;
}
}
}
public Brush ConnectionStatusColor
{
get
{
if (orchestrator == null)
return Brushes.Fuchsia;
switch (orchestrator.DeviceStatus)
{
case MassiveKnobDeviceStatus.Disconnected:
return Brushes.DarkRed;
case MassiveKnobDeviceStatus.Connecting:
return Brushes.Orange;
case MassiveKnobDeviceStatus.Connected:
return Brushes.ForestGreen;
default:
return null;
}
}
}
*/
// ReSharper restore UnusedMember.Global
public SettingsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator, ILoggingSwitch loggingSwitch)
public SettingsViewModel(SimpleInjector.Container container, /*IPluginManager pluginManager, */IMassiveKnobOrchestrator orchestrator/*, ILoggingSwitch loggingSwitch*/)
{
this.container = container;
this.orchestrator = orchestrator;
this.loggingSwitch = loggingSwitch;
//this.loggingSwitch = loggingSwitch;
// For design-time support
if (orchestrator == null)
@ -348,6 +257,7 @@ namespace MassiveKnob.ViewModel
SelectedMenuItem = activeMenuItem;
activeDeviceSubscription = orchestrator.ActiveDeviceSubject.Subscribe(info =>
{
Application.Current?.Dispatcher.Invoke(() =>
@ -355,18 +265,12 @@ namespace MassiveKnob.ViewModel
Specs = info.Specs;
});
});
deviceStatusSubscription = orchestrator.DeviceStatusSubject.Subscribe(status =>
{
OnDependantPropertyChanged(nameof(ConnectionStatusColor));
OnDependantPropertyChanged(nameof(ConnectionStatusText));
});
if (orchestrator.ActiveDevice != null)
Specs = orchestrator.ActiveDevice.Specs;
Devices = pluginManager.GetDevicePlugins()
.SelectMany(dp => dp.Devices.Select(d => new DeviceViewModel(dp, d)))
.OrderBy(d => d.Name.ToLower())
.ToList();
/*
var allActions = new List<ActionViewModel>
{
new ActionViewModel(null, null)
@ -379,12 +283,6 @@ namespace MassiveKnob.ViewModel
Actions = allActions;
if (orchestrator.ActiveDevice != null)
{
selectedDevice = Devices.Single(d => d.Device.DeviceId == orchestrator.ActiveDevice.Info.DeviceId);
SettingsControl = orchestrator.ActiveDevice.Instance.CreateSettingsControl();
Specs = orchestrator.ActiveDevice.Specs;
}
var logSettings = orchestrator.GetSettings().Log;
@ -403,24 +301,23 @@ namespace MassiveKnob.ViewModel
var runKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", false);
runAtStartup = runKey?.GetValue("MassiveKnob") != null;
*/
}
public void Dispose()
{
if (SettingsControl is IDisposable disposable)
disposable.Dispose();
/*
DisposeInputOutputViewModels(AnalogInputs);
DisposeInputOutputViewModels(DigitalInputs);
DisposeInputOutputViewModels(AnalogOutputs);
DisposeInputOutputViewModels(DigitalOutputs);
*/
activeDeviceSubscription?.Dispose();
deviceStatusSubscription?.Dispose();
}
/*
private void ApplyLoggingSettings()
{
orchestrator?.UpdateSettings(settings =>
@ -460,6 +357,7 @@ namespace MassiveKnob.ViewModel
foreach (var viewModel in viewModels)
viewModel.Dispose();
}
*/
public event PropertyChangedEventHandler PropertyChanged;
@ -478,7 +376,7 @@ namespace MassiveKnob.ViewModel
public class SettingsViewModelDesignTime : SettingsViewModel
{
public SettingsViewModelDesignTime() : base(null, null, null)
public SettingsViewModelDesignTime() : base(null, null)
{
Specs = new DeviceSpecs(2, 2, 2, 2);
}