Merge branch 'master' into develop

This commit is contained in:
Mark van Renswoude 2021-03-16 12:40:46 +01:00
commit 6f5e588e13
65 changed files with 1730 additions and 490 deletions

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{BB1E8BA4-7965-4E46-B1BE-D2A7C491A204}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MassiveKnob.Plugin.RunProgram</RootNamespace>
<AssemblyName>MassiveKnob.Plugin.RunProgram</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="MassiveKnobRunProgramPlugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RunProgram\RunProgramSettingsView.xaml.cs">
<DependentUpon>RunProgramSettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="RunProgram\RunProgramAction.cs" />
<Compile Include="RunProgram\RunProgramSettings.cs" />
<Compile Include="RunProgram\RunProgramSettingsViewModel.cs" />
<Compile Include="Strings.Designer.cs">
<DependentUpon>Strings.resx</DependentUpon>
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="MassiveKnobPlugin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
<Project>{A1298BE4-1D23-416C-8C56-FC9264487A95}</Project>
<Name>MassiveKnob.Plugin</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Strings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Threading.Tasks.Extensions">
<Version>4.5.4</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Page Include="RunProgram\RunProgramSettingsView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,3 @@
{
"EntryAssembly": "MassiveKnob.Plugin.RunProgram.dll"
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using MassiveKnob.Plugin.RunProgram.RunProgram;
namespace MassiveKnob.Plugin.RunProgram
{
[MassiveKnobPlugin]
public class MassiveKnobRunProgramPlugin : IMassiveKnobActionPlugin
{
public Guid PluginId { get; } = new Guid("10537f2a-6876-48b8-8ef9-8d05f185fa62");
public string Name { get; } = Strings.PluginName;
public string Description { get; } = Strings.PluginDescription;
public string Author { get; } = "Mark van Renswoude <mark@x2software.net>";
public string Url { get; } = "https://www.github.com/MvRens/MassiveKnob/";
public IEnumerable<IMassiveKnobAction> Actions { get; } = new IMassiveKnobAction[]
{
new RunProgramAction()
};
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MassiveKnob.Plugin.RunProgram")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MassiveKnob.Plugin.RunProgram")]
[assembly: AssemblyCopyright("Copyright © 2021")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("bb1e8ba4-7965-4e46-b1be-d2a7c491a204")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,81 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Controls;
using Microsoft.Extensions.Logging;
namespace MassiveKnob.Plugin.RunProgram.RunProgram
{
public class RunProgramAction : IMassiveKnobAction
{
public Guid ActionId { get; } = new Guid("c3a79015-4b8f-414d-9682-02307de8639c");
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputDigital;
public string Name { get; } = Strings.RunProgramName;
public string Description { get; } = Strings.RunProgramDescription;
public IMassiveKnobActionInstance Create(ILogger logger)
{
return new Instance(logger);
}
private class Instance : IMassiveKnobDigitalAction
{
private readonly ILogger logger;
private IMassiveKnobActionContext actionContext;
private RunProgramSettings settings;
public Instance(ILogger logger)
{
this.logger = logger;
}
public void Initialize(IMassiveKnobActionContext context)
{
actionContext = context;
settings = context.GetSettings<RunProgramSettings>();
}
public void Dispose()
{
}
public UserControl CreateSettingsControl()
{
var viewModel = new RunProgramSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) =>
{
actionContext.SetSettings(settings);
};
return new RunProgramSettingsView(viewModel);
}
public ValueTask DigitalChanged(bool on)
{
if (!on)
return default;
if (string.IsNullOrEmpty(settings.Filename))
return default;
logger.LogInformation("Run program: filename = {filename}, arguments = {arguments}", settings.Filename, settings.Arguments);
Process.Start(new ProcessStartInfo
{
FileName = settings.Filename,
Arguments = settings.Arguments,
UseShellExecute = true,
Verb = "open"
});
return default;
}
}
}
}

View File

@ -0,0 +1,8 @@
namespace MassiveKnob.Plugin.RunProgram.RunProgram
{
public class RunProgramSettings
{
public string Filename { get; set; }
public string Arguments { get; set; }
}
}

View File

@ -0,0 +1,21 @@
<UserControl x:Class="MassiveKnob.Plugin.RunProgram.RunProgram.RunProgramSettingsView"
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:strings="clr-namespace:MassiveKnob.Plugin.RunProgram"
xmlns:runProgram="clr-namespace:MassiveKnob.Plugin.RunProgram.RunProgram"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance runProgram:RunProgramSettingsViewModel}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Static strings:Strings.SettingFilename}" />
<DockPanel>
<Button Click="ButtonBrowseClick" DockPanel.Dock="Right" Padding="4,0,4,0">...</Button>
<TextBox Text="{Binding Filename}" />
</DockPanel>
<TextBlock Text="{x:Static strings:Strings.SettingArguments}" Margin="0,8,0,0" />
<TextBox Text="{Binding Arguments}" />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,27 @@
using System.Windows;
namespace MassiveKnob.Plugin.RunProgram.RunProgram
{
/// <summary>
/// Interaction logic for RunProgramSettingsView.xaml
/// </summary>
public partial class RunProgramSettingsView
{
public RunProgramSettingsView(RunProgramSettingsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
private void ButtonBrowseClick(object sender, RoutedEventArgs e)
{
var dialog = new Microsoft.Win32.OpenFileDialog
{
Filter = Strings.FilenameDialogFilter
};
if (dialog.ShowDialog().GetValueOrDefault())
((RunProgramSettingsViewModel) DataContext).Filename = dialog.FileName;
}
}
}

View File

@ -0,0 +1,53 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MassiveKnob.Plugin.RunProgram.RunProgram
{
public class RunProgramSettingsViewModel : INotifyPropertyChanged
{
private readonly RunProgramSettings settings;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public string Filename
{
get => settings.Filename;
set
{
if (value == settings.Filename)
return;
settings.Filename = value;
OnPropertyChanged();
}
}
public string Arguments
{
get => settings.Arguments;
set
{
if (value == settings.Arguments)
return;
settings.Arguments = value;
OnPropertyChanged();
}
}
// ReSharper restore UnusedMember.Global
public RunProgramSettingsViewModel(RunProgramSettings settings)
{
this.settings = settings;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace MassiveKnob.Plugin.RunProgram {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Strings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Strings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Plugin.RunProgram.Strings", typeof(Strings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Executable files (*.exe, *.bat, *.cmd)|*.exe,*.bat,*.cmd|All files (*.*)|*.*.
/// </summary>
public static string FilenameDialogFilter {
get {
return ResourceManager.GetString("FilenameDialogFilter", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Provides an action to run an application when a button is pressed..
/// </summary>
public static string PluginDescription {
get {
return ResourceManager.GetString("PluginDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Run Program.
/// </summary>
public static string PluginName {
get {
return ResourceManager.GetString("PluginName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Runs an application when a button is pressed..
/// </summary>
public static string RunProgramDescription {
get {
return ResourceManager.GetString("RunProgramDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Run program.
/// </summary>
public static string RunProgramName {
get {
return ResourceManager.GetString("RunProgramName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Optional arguments passed to the executable.
/// </summary>
public static string SettingArguments {
get {
return ResourceManager.GetString("SettingArguments", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Executable, file or URL to open.
/// </summary>
public static string SettingFilename {
get {
return ResourceManager.GetString("SettingFilename", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="FilenameDialogFilter" xml:space="preserve">
<value>Executable files (*.exe, *.bat, *.cmd)|*.exe,*.bat,*.cmd|All files (*.*)|*.*</value>
</data>
<data name="PluginDescription" xml:space="preserve">
<value>Provides an action to run an application when a button is pressed.</value>
</data>
<data name="PluginName" xml:space="preserve">
<value>Run Program</value>
</data>
<data name="RunProgramDescription" xml:space="preserve">
<value>Runs an application when a button is pressed.</value>
</data>
<data name="RunProgramName" xml:space="preserve">
<value>Run program</value>
</data>
<data name="SettingArguments" xml:space="preserve">
<value>Optional arguments passed to the executable</value>
</data>
<data name="SettingFilename" xml:space="preserve">
<value>Executable, file or URL to open</value>
</data>
</root>

View File

@ -1,7 +1,5 @@
using System;
// ReSharper disable UnusedMember.Global - public API
namespace MassiveKnob.Plugin
{
/// <summary>

View File

@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.VoiceMee
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Voicemeeter", "VoicemeeterRemote\Voicemeeter\Voicemeeter.csproj", "{F35DD8E5-91FA-403E-B6F6-8D2B4AE84198}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.RunProgram", "MassiveKnob.Plugin.RunProgram\MassiveKnob.Plugin.RunProgram.csproj", "{BB1E8BA4-7965-4E46-B1BE-D2A7C491A204}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -63,6 +65,10 @@ Global
{F35DD8E5-91FA-403E-B6F6-8D2B4AE84198}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F35DD8E5-91FA-403E-B6F6-8D2B4AE84198}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F35DD8E5-91FA-403E-B6F6-8D2B4AE84198}.Release|Any CPU.Build.0 = Release|Any CPU
{BB1E8BA4-7965-4E46-B1BE-D2A7C491A204}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB1E8BA4-7965-4E46-B1BE-D2A7C491A204}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB1E8BA4-7965-4E46-B1BE-D2A7C491A204}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB1E8BA4-7965-4E46-B1BE-D2A7C491A204}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

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,30 @@
<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>
<Style TargetType="TextBlock" x:Key="PluginAuthor">
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
<Setter Property="Margin" Value="0,8,0,0" />
</Style>
<Style TargetType="TextBlock" x:Key="PluginUrl">
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
<Setter Property="Foreground" Value="Navy" />
<Setter Property="Margin" Value="0,0,0,8" />
</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,31 @@
<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 Author}" Visibility="{Binding AuthorVisibility}" Style="{StaticResource PluginAuthor}" />
<TextBlock Text="{Binding Url}" Cursor="{x:Static Cursors.Hand}" MouseDown="UrlMouseDown" Visibility="{Binding UrlVisibility}" Style="{StaticResource PluginUrl}" />
<TextBlock Text="{Binding Filename}" Style="{StaticResource PluginFilename}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>

View File

@ -0,0 +1,31 @@
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
using MassiveKnob.ViewModel;
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();
}
private void UrlMouseDown(object sender, MouseButtonEventArgs e)
{
var dataContext = ((FrameworkElement) e.Source).DataContext;
Process.Start(new ProcessStartInfo
{
FileName = ((PluginViewModel) dataContext).Url,
Verb = "open"
});
}
}
}

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,29 @@
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 string Author { get; }
public string Url { get; }
public Visibility DescriptionVisibility => string.IsNullOrEmpty(Description) ? Visibility.Collapsed : Visibility.Visible;
public Visibility AuthorVisibility => string.IsNullOrEmpty(Author) ? Visibility.Collapsed : Visibility.Visible;
public Visibility UrlVisibility => string.IsNullOrEmpty(Url) ? Visibility.Collapsed : Visibility.Visible;
// ReSharper restore UnusedMember.Global
public PluginViewModel(string name, string description, string filename, string author, string url)
{
Name = name;
Description = description;
Filename = filename;
Author = author;
Url = url;
}
}
}

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, p.Plugin.Author, p.Plugin.Url))
.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", "Some Massive Knob <massive@knob.org>", "https://lmgtfy.app/?q=Massive+Knob"),
new PluginViewModel("Design-time plugin", "Fake plugin only visible at design-time.", "C:\\Does\\Not\\Exist.dll", null, null)
};
}
}
}

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);
}

View File

@ -36,6 +36,7 @@ Name: essentialplugins; Description: "Essential plugins"; Types: full custom
Name: essentialplugins\serialdevice; Description: "Serial device"; Types: full custom
Name: essentialplugins\coreaudio; Description: "Windows Core Audio actions"; Types: full custom
Name: optionalplugins; Description: "Optional plugins"; Types: full custom
Name: optionalplugins\runprogram; Description: "Run program"; Types: full custom
Name: optionalplugins\emulatordevice; Description: "Emulator device"; Types: full custom
Name: optionalplugins\voicemeeter; Description: "VoiceMeeter actions"; Types: full custom
@ -65,6 +66,10 @@ Source: {#BasePath}\MassiveKnob.Plugin.EmulatorDevice\bin\Release\*.dll; DestDir
Source: {#BasePath}\MassiveKnob.Plugin.VoiceMeeter\bin\Release\MassiveKnobPlugin.json; DestDir: "{app}\Plugins\VoiceMeeter"; Flags: ignoreversion; Components: optionalplugins\voicemeeter
Source: {#BasePath}\MassiveKnob.Plugin.VoiceMeeter\bin\Release\*.dll; DestDir: "{app}\Plugins\VoiceMeeter"; Flags: ignoreversion; Components: optionalplugins\voicemeeter
; Run Program plugin
Source: {#BasePath}\MassiveKnob.Plugin.RunProgram\bin\Release\MassiveKnobPlugin.json; DestDir: "{app}\Plugins\RunProgram"; Flags: ignoreversion; Components: optionalplugins\runprogram
Source: {#BasePath}\MassiveKnob.Plugin.RunProgram\bin\Release\*.dll; DestDir: "{app}\Plugins\RunProgram"; Flags: ignoreversion; Components: optionalplugins\runprogram
[Dirs]
Name: "{localappdata}\MassiveKnob"
Name: "{localappdata}\MassiveKnob\Logs"