1
0
mirror of synced 2024-12-04 16:43:08 +01:00

Implemented OSD

Implemented mute/unmute input and output actions
Changed MockDevice to EmulatorDevice with UI for easier testing
This commit is contained in:
Mark van Renswoude 2021-02-24 19:35:01 +01:00
parent ff1e1ca74c
commit 28c25c8b43
67 changed files with 2357 additions and 334 deletions

View File

@ -16,8 +16,8 @@ Because of the second requirement, a simple media keys HID device does not suffi
- by changing the Windows default output device - by changing the Windows default output device
- by running a VoiceMeeter macro - by running a VoiceMeeter macro
2. Corresponding LEDs to indicate the currently active device 2. Corresponding LEDs to indicate the currently active device
3. OSD 3. OSD
4. API / plugins to use extra knobs and buttons for other purposes 4. API / plugins to use extra knobs and buttons for other purposes
## Developing ## Developing
The hardware side uses an Arduino sketch to communicate the hardware state over the serial port. The hardware side uses an Arduino sketch to communicate the hardware state over the serial port.

View File

@ -0,0 +1,12 @@
using System;
namespace MassiveKnob.Plugin.CoreAudio.Base
{
public class BaseDeviceSettings
{
public Guid? DeviceId { get; set; }
// TODO more options, like positioning and style
public bool OSD { get; set; } = true;
}
}

View File

@ -0,0 +1,19 @@
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.Base.BaseDeviceSettingsView"
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:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
xmlns:coreAudio="clr-namespace:MassiveKnob.Plugin.CoreAudio"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance base:BaseDeviceSettingsViewModel}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Static coreAudio:Strings.SettingPlaybackDevice}" />
<ComboBox Margin="0,4,0,0" ItemsSource="{Binding PlaybackDevices}" SelectedItem="{Binding SelectedDevice}" DisplayMemberPath="DisplayName" />
<CheckBox Margin="0,8,0,0" IsChecked="{Binding OSD}">
<TextBlock Text="{x:Static coreAudio:Strings.SettingOSD}" />
</CheckBox>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MassiveKnob.Plugin.CoreAudio.Base
{
/// <summary>
/// Interaction logic for BaseDeviceSettingsView.xaml
/// </summary>
public partial class BaseDeviceSettingsView : UserControl
{
public BaseDeviceSettingsView()
{
InitializeComponent();
}
}
}

View File

@ -7,11 +7,22 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using AudioSwitcher.AudioApi; using AudioSwitcher.AudioApi;
namespace MassiveKnob.Plugin.CoreAudio.Settings namespace MassiveKnob.Plugin.CoreAudio.Base
{ {
public class BaseDeviceSettingsViewModel<T> : BaseDeviceSettingsViewModel where T : BaseDeviceSettings
{
protected new T Settings => (T)base.Settings;
public BaseDeviceSettingsViewModel(T settings) : base(settings)
{
}
}
public class BaseDeviceSettingsViewModel : INotifyPropertyChanged public class BaseDeviceSettingsViewModel : INotifyPropertyChanged
{ {
private readonly BaseDeviceSettings settings; protected readonly BaseDeviceSettings Settings;
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
private IList<PlaybackDeviceViewModel> playbackDevices; private IList<PlaybackDeviceViewModel> playbackDevices;
@ -37,7 +48,21 @@ namespace MassiveKnob.Plugin.CoreAudio.Settings
return; return;
selectedDevice = value; selectedDevice = value;
settings.DeviceId = selectedDevice?.Id; Settings.DeviceId = selectedDevice?.Id;
OnPropertyChanged();
}
}
public bool OSD
{
get => Settings.OSD;
set
{
if (value == Settings.OSD)
return;
Settings.OSD = value;
OnPropertyChanged(); OnPropertyChanged();
} }
} }
@ -46,7 +71,7 @@ namespace MassiveKnob.Plugin.CoreAudio.Settings
public BaseDeviceSettingsViewModel(BaseDeviceSettings settings) public BaseDeviceSettingsViewModel(BaseDeviceSettings settings)
{ {
this.settings = settings; Settings = settings;
Task.Run(async () => Task.Run(async () =>
{ {

View File

@ -0,0 +1,82 @@
using System;
using System.Windows.Controls;
using AudioSwitcher.AudioApi;
using MassiveKnob.Plugin.CoreAudio.OSD;
namespace MassiveKnob.Plugin.CoreAudio.GetMuted
{
public class DeviceGetMutedAction : IMassiveKnobAction
{
public Guid ActionId { get; } = new Guid("86646ca7-f472-4c5a-8d0f-7e5d2d162ab9");
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputDigital;
public string Name { get; } = Strings.GetMutedName;
public string Description { get; } = Strings.GetMutedDescription;
public IMassiveKnobActionInstance Create()
{
return new Instance();
}
private class Instance : IMassiveKnobActionInstance
{
private IMassiveKnobActionContext actionContext;
private DeviceGetMutedActionSettings settings;
private IDevice playbackDevice;
private IDisposable deviceChanged;
public void Initialize(IMassiveKnobActionContext context)
{
actionContext = context;
settings = context.GetSettings<DeviceGetMutedActionSettings>();
ApplySettings();
}
public void Dispose()
{
deviceChanged?.Dispose();
}
private void ApplySettings()
{
if (playbackDevice != null && playbackDevice.Id == settings.DeviceId)
return;
var coreAudioController = CoreAudioControllerInstance.Acquire();
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
deviceChanged?.Dispose();
deviceChanged = playbackDevice?.MuteChanged.Subscribe(MuteChanged);
}
public UserControl CreateSettingsControl()
{
var viewModel = new DeviceGetMutedActionSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) =>
{
if (!viewModel.IsSettingsProperty(args.PropertyName))
return;
actionContext.SetSettings(settings);
ApplySettings();
};
return new DeviceGetMutedActionSettingsView(viewModel);
}
private void MuteChanged(DeviceMuteChangedArgs args)
{
actionContext.SetDigitalOutput(settings.Inverted ? !args.IsMuted : args.IsMuted);
if (settings.OSD)
OSDManager.Show(args.Device);
}
}
}
}

View File

@ -0,0 +1,9 @@
using MassiveKnob.Plugin.CoreAudio.Base;
namespace MassiveKnob.Plugin.CoreAudio.GetMuted
{
public class DeviceGetMutedActionSettings : BaseDeviceSettings
{
public bool Inverted { get; set; }
}
}

View File

@ -1,14 +1,14 @@
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.Settings.DeviceVolumeActionSettingsView" <UserControl x:Class="MassiveKnob.Plugin.CoreAudio.GetMuted.DeviceGetMutedActionSettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:settings="clr-namespace:MassiveKnob.Plugin.CoreAudio.Settings" xmlns:getMuted="clr-namespace:MassiveKnob.Plugin.CoreAudio.GetMuted"
xmlns:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800" d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance settings:DeviceVolumeActionSettingsViewModel}"> d:DataContext="{d:DesignInstance getMuted:DeviceGetMutedActionSettingsViewModel}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock>Playback device</TextBlock> <base:BaseDeviceSettingsView />
<ComboBox ItemsSource="{Binding PlaybackDevices}" SelectedItem="{Binding SelectedDevice}" DisplayMemberPath="DisplayName" />
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@ -0,0 +1,14 @@
namespace MassiveKnob.Plugin.CoreAudio.GetMuted
{
/// <summary>
/// Interaction logic for DeviceGetMutedActionSettingsView.xaml
/// </summary>
public partial class DeviceGetMutedActionSettingsView
{
public DeviceGetMutedActionSettingsView(DeviceGetMutedActionSettingsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,28 @@
using MassiveKnob.Plugin.CoreAudio.Base;
namespace MassiveKnob.Plugin.CoreAudio.GetMuted
{
public class DeviceGetMutedActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceGetMutedActionSettings>
{
// ReSharper disable UnusedMember.Global - used by WPF Binding
public bool Inverted
{
get => Settings.Inverted;
set
{
if (value == Settings.Inverted)
return;
Settings.Inverted = value;
OnPropertyChanged();
}
}
// ReSharper restore UnusedMember.Global
// ReSharper disable once SuggestBaseTypeForParameter - by design
public DeviceGetMutedActionSettingsViewModel(DeviceGetMutedActionSettings settings) : base(settings)
{
}
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Windows.Controls;
using AudioSwitcher.AudioApi;
using MassiveKnob.Plugin.CoreAudio.OSD;
namespace MassiveKnob.Plugin.CoreAudio.GetVolume
{
public class DeviceGetVolumeAction : IMassiveKnobAction
{
public Guid ActionId { get; } = new Guid("6ebf91af-8240-4a75-9729-c6a1eb60dcba");
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputAnalog;
public string Name { get; } = Strings.GetVolumeName;
public string Description { get; } = Strings.GetVolumeDescription;
public IMassiveKnobActionInstance Create()
{
return new Instance();
}
private class Instance : IMassiveKnobActionInstance
{
private IMassiveKnobActionContext actionContext;
private DeviceGetVolumeActionSettings settings;
private IDevice playbackDevice;
private IDisposable deviceChanged;
public void Initialize(IMassiveKnobActionContext context)
{
actionContext = context;
settings = context.GetSettings<DeviceGetVolumeActionSettings>();
ApplySettings();
}
public void Dispose()
{
deviceChanged?.Dispose();
}
private void ApplySettings()
{
if (playbackDevice != null && playbackDevice.Id == settings.DeviceId)
return;
var coreAudioController = CoreAudioControllerInstance.Acquire();
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
deviceChanged?.Dispose();
deviceChanged = playbackDevice?.VolumeChanged.Subscribe(VolumeChanged);
}
public UserControl CreateSettingsControl()
{
var viewModel = new DeviceGetVolumeActionSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) =>
{
if (!viewModel.IsSettingsProperty(args.PropertyName))
return;
actionContext.SetSettings(settings);
ApplySettings();
};
return new DeviceGetVolumeActionSettingsView(viewModel);
}
private void VolumeChanged(DeviceVolumeChangedArgs args)
{
actionContext.SetAnalogOutput((byte)args.Volume);
if (settings.OSD)
OSDManager.Show(args.Device);
}
}
}
}

View File

@ -0,0 +1,8 @@
using MassiveKnob.Plugin.CoreAudio.Base;
namespace MassiveKnob.Plugin.CoreAudio.GetVolume
{
public class DeviceGetVolumeActionSettings : BaseDeviceSettings
{
}
}

View File

@ -0,0 +1,14 @@
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.GetVolume.DeviceGetVolumeActionSettingsView"
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:getVolume="clr-namespace:MassiveKnob.Plugin.CoreAudio.GetVolume"
xmlns:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance getVolume:DeviceGetVolumeActionSettingsViewModel}">
<StackPanel Orientation="Vertical">
<base:BaseDeviceSettingsView />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,16 @@
using MassiveKnob.Plugin.CoreAudio.SetMuted;
namespace MassiveKnob.Plugin.CoreAudio.GetVolume
{
/// <summary>
/// Interaction logic for DeviceGetVolumeActionSettingsView.xaml
/// </summary>
public partial class DeviceGetVolumeActionSettingsView
{
public DeviceGetVolumeActionSettingsView(DeviceGetVolumeActionSettingsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,12 @@
using MassiveKnob.Plugin.CoreAudio.Base;
namespace MassiveKnob.Plugin.CoreAudio.GetVolume
{
public class DeviceGetVolumeActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceGetVolumeActionSettings>
{
// ReSharper disable once SuggestBaseTypeForParameter - by design
public DeviceGetVolumeActionSettingsViewModel(DeviceGetVolumeActionSettings settings) : base(settings)
{
}
}
}

View File

@ -35,6 +35,7 @@
<Reference Include="PresentationFramework" /> <Reference Include="PresentationFramework" />
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xaml" /> <Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
@ -45,17 +46,43 @@
<Reference Include="WindowsBase" /> <Reference Include="WindowsBase" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Actions\DeviceVolumeAction.cs" /> <Compile Include="Base\BaseDeviceSettingsView.xaml.cs">
<DependentUpon>BaseDeviceSettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="GetMuted\DeviceGetMutedAction.cs" />
<Compile Include="GetMuted\DeviceGetMutedActionSettings.cs" />
<Compile Include="GetMuted\DeviceGetMutedActionSettingsView.xaml.cs">
<DependentUpon>DeviceGetMutedActionSettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="GetMuted\DeviceGetMutedActionSettingsViewModel.cs" />
<Compile Include="GetVolume\DeviceGetVolumeAction.cs" />
<Compile Include="OSD\OSDWindow.xaml.cs">
<DependentUpon>OSDWindow.xaml</DependentUpon>
</Compile>
<Compile Include="OSD\OSDManager.cs" />
<Compile Include="OSD\OSDWindowViewModel.cs" />
<Compile Include="SetMuted\DeviceSetMutedAction.cs" />
<Compile Include="SetMuted\DeviceSetMutedActionSettings.cs" />
<Compile Include="SetMuted\DeviceSetMutedActionSettingsView.xaml.cs">
<DependentUpon>DeviceSetMutedActionSettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="SetMuted\DeviceSetMutedActionSettingsViewModel.cs" />
<Compile Include="SetVolume\DeviceSetVolumeAction.cs" />
<Compile Include="CoreAudioControllerInstance.cs" /> <Compile Include="CoreAudioControllerInstance.cs" />
<Compile Include="GetVolume\DeviceGetVolumeActionSettingsViewModel.cs" />
<Compile Include="GetVolume\DeviceGetVolumeActionSettingsView.xaml.cs">
<DependentUpon>DeviceGetVolumeActionSettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="MassiveKnobCoreAudioPlugin.cs" /> <Compile Include="MassiveKnobCoreAudioPlugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Settings\DeviceVolumeActionSettings.cs" /> <Compile Include="GetVolume\DeviceGetVolumeActionSettings.cs" />
<Compile Include="Settings\DeviceVolumeActionSettingsView.xaml.cs"> <Compile Include="SetVolume\DeviceSetVolumeActionSettings.cs" />
<DependentUpon>DeviceVolumeActionSettingsView.xaml</DependentUpon> <Compile Include="SetVolume\DeviceSetVolumeActionSettingsView.xaml.cs">
<DependentUpon>DeviceSetVolumeActionSettingsView.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Settings\BaseDeviceSettings.cs" /> <Compile Include="Base\BaseDeviceSettings.cs" />
<Compile Include="Settings\BaseDeviceSettingsViewModel.cs" /> <Compile Include="Base\BaseDeviceSettingsViewModel.cs" />
<Compile Include="Settings\DeviceVolumeActionSettingsViewModel.cs" /> <Compile Include="SetVolume\DeviceSetVolumeActionSettingsViewModel.cs" />
<Compile Include="Strings.Designer.cs"> <Compile Include="Strings.Designer.cs">
<DependentUpon>Strings.resx</DependentUpon> <DependentUpon>Strings.resx</DependentUpon>
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
@ -72,21 +99,49 @@
<PackageReference Include="AudioSwitcher.AudioApi.CoreAudio"> <PackageReference Include="AudioSwitcher.AudioApi.CoreAudio">
<Version>4.0.0-alpha5</Version> <Version>4.0.0-alpha5</Version>
</PackageReference> </PackageReference>
<PackageReference Include="System.Reactive">
<Version>5.0.0</Version>
</PackageReference>
<PackageReference Include="System.Threading.Tasks.Extensions"> <PackageReference Include="System.Threading.Tasks.Extensions">
<Version>4.5.4</Version> <Version>4.5.4</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Strings.resx"> <EmbeddedResource Include="Strings.resx">
<Generator>ResXFileCodeGenerator</Generator> <Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput> <LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Include="Settings\DeviceVolumeActionSettingsView.xaml"> <Page Include="Base\BaseDeviceSettingsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="GetMuted\DeviceGetMutedActionSettingsView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="GetVolume\DeviceGetVolumeActionSettingsView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="OSD\OSDWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="OSD\SpeakerIcon.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="SetMuted\DeviceSetMutedActionSettingsView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="SetVolume\DeviceSetVolumeActionSettingsView.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@ -1,7 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using MassiveKnob.Plugin.CoreAudio.Actions; using MassiveKnob.Plugin.CoreAudio.GetMuted;
using MassiveKnob.Plugin.CoreAudio.GetVolume;
using MassiveKnob.Plugin.CoreAudio.SetMuted;
using MassiveKnob.Plugin.CoreAudio.SetVolume;
namespace MassiveKnob.Plugin.CoreAudio namespace MassiveKnob.Plugin.CoreAudio
{ {
@ -9,14 +12,17 @@ namespace MassiveKnob.Plugin.CoreAudio
public class MassiveKnobCoreAudioPlugin : IMassiveKnobActionPlugin public class MassiveKnobCoreAudioPlugin : IMassiveKnobActionPlugin
{ {
public Guid PluginId { get; } = new Guid("eaa5d3f8-8f9b-4a4b-8e29-827228d23e95"); public Guid PluginId { get; } = new Guid("eaa5d3f8-8f9b-4a4b-8e29-827228d23e95");
public string Name { get; } = "Windows Core Audio"; public string Name { get; } = Strings.PluginName;
public string Description { get; } = "Included with Massive Knob by default. Provides volume control per device and default device switching."; public string Description { get; } = Strings.PluginDescription;
public string Author { get; } = "Mark van Renswoude <mark@x2software.net>"; public string Author { get; } = "Mark van Renswoude <mark@x2software.net>";
public string Url { get; } = "https://www.github.com/MvRens/MassiveKnob/"; public string Url { get; } = "https://www.github.com/MvRens/MassiveKnob/";
public IEnumerable<IMassiveKnobAction> Actions { get; } = new IMassiveKnobAction[] public IEnumerable<IMassiveKnobAction> Actions { get; } = new IMassiveKnobAction[]
{ {
new DeviceVolumeAction() new DeviceSetVolumeAction(),
new DeviceGetVolumeAction(),
new DeviceSetMutedAction(),
new DeviceGetMutedAction()
}; };

View File

@ -0,0 +1,50 @@
using System.Threading;
using System.Windows;
using AudioSwitcher.AudioApi;
namespace MassiveKnob.Plugin.CoreAudio.OSD
{
public static class OSDManager
{
private const int OSDTimeout = 2500;
private static OSDWindowViewModel windowViewModel;
private static Window window;
private static Timer hideTimer;
public static void Show(IDevice device)
{
Application.Current.Dispatcher.Invoke(() =>
{
if (window == null)
{
windowViewModel = new OSDWindowViewModel();
window = new OSDWindow(windowViewModel);
hideTimer = new Timer(state =>
{
Hide();
}, null, OSDTimeout, Timeout.Infinite);
}
else
hideTimer.Change(OSDTimeout, Timeout.Infinite);
windowViewModel.SetDevice(device);
window.Show();
});
}
private static void Hide()
{
Application.Current.Dispatcher.Invoke(() =>
{
window?.Close();
window = null;
windowViewModel = null;
hideTimer?.Dispose();
hideTimer = null;
});
}
}
}

View File

@ -0,0 +1,55 @@
<Window x:Class="MassiveKnob.Plugin.CoreAudio.OSD.OSDWindow"
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"
xmlns:osd="clr-namespace:MassiveKnob.Plugin.CoreAudio.OSD"
mc:Ignorable="d"
Title="Massive Knob OSD" Height="60" Width="360"
WindowStartupLocation="Manual" WindowStyle="None" AllowsTransparency="True" ShowInTaskbar="False" Topmost="True"
Loaded="OSDWindow_OnLoaded" Closing="OSDWindow_OnClosing"
d:DataContext="{d:DesignInstance osd:OSDWindowViewModel}">
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:0.250" FillBehavior="HoldEnd" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="SpeakerIcon.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- ReSharper disable once Xaml.RedundantResource - used in runtime -->
<Storyboard x:Key="CloseStoryboard" Completed="CloseStoryboard_Completed">
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:0.250" FillBehavior="HoldEnd" />
</Storyboard>
<Style TargetType="DockPanel" x:Key="OSDWindow">
<Setter Property="Background" Value="#2d2d30" />
</Style>
<Style TargetType="TextBlock" x:Key="DeviceName">
<Setter Property="Foreground" Value="White" />
<Setter Property="Margin" Value="8,4,8,4" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="TextTrimming" Value="CharacterEllipsis"></Setter>
</Style>
<Style x:Key="SpeakerIconStyle">
<Setter Property="Control.Margin" Value="8,4,8,4" />
</Style>
</ResourceDictionary>
</Window.Resources>
<DockPanel Style="{StaticResource OSDWindow}">
<TextBlock DockPanel.Dock="Top" Text="{Binding DeviceName}" Style="{StaticResource DeviceName}"></TextBlock>
<ContentControl DockPanel.Dock="Left" Content="{StaticResource SpeakerIcon}" Style="{StaticResource SpeakerIconStyle}" />
<Canvas Width="300" Height="20" Margin="8,0,8,0">
<Line X1="0" X2="300" Y1="10" Y2="10" Stroke="#80FFFFFF" StrokeThickness="2" />
<Line X1="{Binding VolumeIndicatorLeft}" X2="{Binding VolumeIndicatorLeft}" Y1="0" Y2="20" Stroke="White" StrokeThickness="2" />
</Canvas>
</DockPanel>
</Window>

View File

@ -0,0 +1,49 @@
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Media.Animation;
namespace MassiveKnob.Plugin.CoreAudio.OSD
{
/// <summary>
/// Interaction logic for OSDWindow.xaml
/// </summary>
public partial class OSDWindow
{
private bool closeStoryBoardCompleted;
public OSDWindow(OSDWindowViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
private void OSDWindow_OnLoaded(object sender, RoutedEventArgs e)
{
var desktopArea = Screen.PrimaryScreen.WorkingArea;
Left = (desktopArea.Width - Width) / 2;
Top = desktopArea.Bottom - Height - 25;
}
private void OSDWindow_OnClosing(object sender, CancelEventArgs e)
{
if (closeStoryBoardCompleted)
return;
((Storyboard)FindResource("CloseStoryboard")).Begin(this);
e.Cancel = true;
}
private void CloseStoryboard_Completed(object sender, EventArgs e)
{
closeStoryBoardCompleted = true;
Close();
}
}
}

View File

@ -0,0 +1,92 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Security.RightsManagement;
using System.Windows;
using AudioSwitcher.AudioApi;
namespace MassiveKnob.Plugin.CoreAudio.OSD
{
public class OSDWindowViewModel : INotifyPropertyChanged
{
// ReSharper disable UnusedMember.Global - used by WPF Binding
private string deviceName;
public string DeviceName
{
get => deviceName;
set
{
if (value == deviceName)
return;
deviceName = value;
OnPropertyChanged();
}
}
private int volume;
public int Volume
{
get => volume;
set
{
if (value == volume)
return;
volume = value;
OnPropertyChanged();
OnDependantPropertyChanged(nameof(VolumeLowVisibility));
OnDependantPropertyChanged(nameof(VolumeMediumVisibility));
OnDependantPropertyChanged(nameof(VolumeHighVisibility));
OnDependantPropertyChanged(nameof(VolumeIndicatorLeft));
}
}
private bool isMuted;
public bool IsMuted
{
get => isMuted;
set
{
if (value == isMuted)
return;
isMuted = value;
OnPropertyChanged();
OnDependantPropertyChanged(nameof(IsMutedVisibility));
OnDependantPropertyChanged(nameof(IsNotMutedVisibility));
}
}
public Visibility IsMutedVisibility => IsMuted ? Visibility.Visible : Visibility.Collapsed;
public Visibility IsNotMutedVisibility => IsMuted ? Visibility.Collapsed : Visibility.Visible;
public Visibility VolumeLowVisibility => Volume > 0 ? Visibility.Visible : Visibility.Collapsed;
public Visibility VolumeMediumVisibility => Volume > 33 ? Visibility.Visible : Visibility.Collapsed;
public Visibility VolumeHighVisibility => Volume > 66 ? Visibility.Visible : Visibility.Collapsed;
public int VolumeIndicatorLeft => Volume * 3;
// ReSharper enable UnusedMember.Global
public void SetDevice(IDevice device)
{
DeviceName = device.FullName;
Volume = (int)device.Volume;
IsMuted = device.IsMuted;
}
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));
}
}
}

View File

@ -0,0 +1,52 @@
<!--
AI saved to SVG, converted to XAML using Inkscape, then modified manually to provide
interactivity. Be aware of this when overwriting this file.
-->
<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
xmlns:osd="clr-namespace:MassiveKnob.Plugin.CoreAudio.OSD"
Stretch="Uniform"
x:Key="SpeakerIcon"
d:DataContext="{d:DesignInstance osd:OSDWindowViewModel}">
<Canvas Width="256" Height="256">
<Polygon Visibility="{Binding IsNotMutedVisibility}" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Points=" 133.5,215.101 61.75,168 8.75,168 8.75,88 61.75,88 133.5,40.899 " Name="Speaker_1_" FillRule="NonZero" StrokeThickness="12" Stroke="#FFFFFFFF" StrokeMiterLimit="10" StrokeLineJoin="Round"/>
<Path Visibility="{Binding VolumeLowVisibility}" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Name="Low" StrokeThickness="12" Stroke="#FFFFFFFF" StrokeMiterLimit="10" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<PathGeometry Figures=" M166.806 86c0 0 12.528 15.833 12.528 40.167s-12.528 43.823-12.528 43.823" FillRule="NonZero"/>
</Path.Data>
</Path>
<Path Visibility="{Binding VolumeMediumVisibility}" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Name="Medium" StrokeThickness="12" Stroke="#FFFFFFFF" StrokeMiterLimit="10" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<PathGeometry Figures=" M188.479 57c0 0 21.183 26.769 21.183 67.91c0 41.141-21.183 74.089-21.183 74.089" FillRule="NonZero"/>
</Path.Data>
</Path>
<Path Visibility="{Binding VolumeHighVisibility}" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Name="High" StrokeThickness="12" Stroke="#FFFFFFFF" StrokeMiterLimit="10" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<PathGeometry Figures=" M216.737 35.517c0 0 27.944 35.316 27.944 89.593s-27.944 97.75-27.944 97.75" FillRule="NonZero"/>
</Path.Data>
</Path>
<Path Visibility="{Binding IsMutedVisibility}" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Name="path4561" Fill="#FFFFFFFF">
<Path.Data>
<PathGeometry Figures="M160.503 221.101c-1.717 0-3.421-0.732-4.608-2.153L10.395 44.746c-2.125-2.543-1.785-6.327 0.759-8.451 c2.544-2.125 6.328-1.784 8.451 0.759l145.5 174.201c2.124 2.544 1.784 6.327-0.759 8.452 C163.224 220.644 161.859 221.101 160.503 221.101z" FillRule="NonZero"/>
</Path.Data>
</Path>
<Path Visibility="{Binding IsMutedVisibility}" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Name="path4563" Fill="#FFFFFFFF">
<Path.Data>
<PathGeometry Figures="M127.5 203.984l-62.458-41C64.064 162.342 62.92 162 61.75 162h-47V94h28.967L33.694 82H8.75c-3.313 0-6 2.687-6 6v80 c0 3.313 2.687 6 6 6h51.207l70.25 46.116c0.997 0.654 2.144 0.984 3.293 0.984c0.979 0 1.958-0.238 2.85-0.72 c1.94-1.048 3.15-3.075 3.15-5.28v-6.423l-12-14.367V203.984z" FillRule="NonZero"/>
</Path.Data>
</Path>
<Path Visibility="{Binding IsMutedVisibility}" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Name="path4565" Fill="#FFFFFFFF">
<Path.Data>
<PathGeometry Figures="M127.5 52.016v104.856l12 14.367V40.899c0-2.205-1.21-4.232-3.15-5.28c-1.939-1.047-4.299-0.947-6.143 0.264L63.19 79.877 l7.744 9.271L127.5 52.016z" FillRule="NonZero"/>
</Path.Data>
</Path>
</Canvas>
</Viewbox>
</ResourceDictionary>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="256px"
height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
<g id="Speaker" display="none">
<polygon id="Speaker_1_" display="inline" fill="none" stroke="#000000" stroke-width="12" stroke-linejoin="round" stroke-miterlimit="10" points="
133.5,215.101 61.75,168 8.75,168 8.75,88 61.75,88 133.5,40.899 "/>
<path id="Low" display="inline" fill="none" stroke="#000000" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
M166.806,86c0,0,12.528,15.833,12.528,40.167s-12.528,43.823-12.528,43.823"/>
<path id="Medium" display="inline" fill="none" stroke="#000000" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
M188.479,57c0,0,21.183,26.769,21.183,67.91c0,41.141-21.183,74.089-21.183,74.089"/>
<path id="High" display="inline" fill="none" stroke="#000000" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
M216.737,35.517c0,0,27.944,35.316,27.944,89.593s-27.944,97.75-27.944,97.75"/>
</g>
<g id="Speaker_outline" display="none">
<path display="inline" d="M133.5,221.101c-1.149,0-2.296-0.33-3.293-0.984L59.957,174H8.75c-3.313,0-6-2.687-6-6V88
c0-3.313,2.687-6,6-6h51.207l70.25-46.116c1.844-1.211,4.203-1.312,6.143-0.264c1.94,1.047,3.15,3.075,3.15,5.28v174.201
c0,2.205-1.21,4.232-3.15,5.28C135.458,220.862,134.479,221.101,133.5,221.101z M14.75,162h47c1.17,0,2.314,0.342,3.292,0.984
l62.458,41V52.016l-62.458,41C64.064,93.658,62.92,94,61.75,94h-47V162z"/>
<path display="inline" d="M166.8,175.99c-1.11,0-2.234-0.309-3.238-0.954c-2.788-1.792-3.595-5.504-1.803-8.291
c0.109-0.173,11.575-18.406,11.575-40.579c0-21.992-11.121-36.301-11.233-36.443c-2.057-2.599-1.616-6.372,0.982-8.428
c2.598-2.057,6.371-1.617,8.428,0.982c0.564,0.713,13.823,17.77,13.823,43.89c0,25.799-12.931,46.21-13.481,47.067
C170.706,175.018,168.773,175.99,166.8,175.99z"/>
<path display="inline" d="M188.473,205c-1.111,0-2.234-0.309-3.239-0.954c-2.787-1.792-3.594-5.504-1.802-8.292
c0.199-0.311,20.229-32.06,20.229-70.844c0-38.607-19.688-63.935-19.888-64.187c-2.057-2.599-1.616-6.372,0.981-8.428
c2.602-2.057,6.373-1.616,8.429,0.982c0.918,1.16,22.478,28.897,22.478,71.633c0,42.416-21.231,75.928-22.136,77.334
C192.379,204.027,190.446,205,188.473,205z"/>
<path display="inline" d="M216.731,228.861c-1.11,0-2.234-0.309-3.238-0.954c-2.788-1.791-3.595-5.504-1.803-8.291
c0.267-0.418,26.991-42.736,26.991-94.506c0-51.593-26.383-85.533-26.649-85.87c-2.057-2.599-1.616-6.372,0.982-8.428
c2.599-2.057,6.371-1.616,8.428,0.982c1.194,1.509,29.239,37.593,29.239,93.316c0,55.402-27.717,99.16-28.897,100.995
C220.638,227.889,218.705,228.861,216.731,228.861z"/>
</g>
<g id="Muted">
<path d="M160.503,221.101c-1.717,0-3.421-0.732-4.608-2.153L10.395,44.746c-2.125-2.543-1.785-6.327,0.759-8.451
c2.544-2.125,6.328-1.784,8.451,0.759l145.5,174.201c2.124,2.544,1.784,6.327-0.759,8.452
C163.224,220.644,161.859,221.101,160.503,221.101z"/>
<path d="M127.5,203.984l-62.458-41C64.064,162.342,62.92,162,61.75,162h-47V94h28.967L33.694,82H8.75c-3.313,0-6,2.687-6,6v80
c0,3.313,2.687,6,6,6h51.207l70.25,46.116c0.997,0.654,2.144,0.984,3.293,0.984c0.979,0,1.958-0.238,2.85-0.72
c1.94-1.048,3.15-3.075,3.15-5.28v-6.423l-12-14.367V203.984z"/>
<path d="M127.5,52.016v104.856l12,14.367V40.899c0-2.205-1.21-4.232-3.15-5.28c-1.939-1.047-4.299-0.947-6.143,0.264L63.19,79.877
l7.744,9.271L127.5,52.016z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using System.Windows.Controls;
using AudioSwitcher.AudioApi;
using MassiveKnob.Plugin.CoreAudio.OSD;
namespace MassiveKnob.Plugin.CoreAudio.SetMuted
{
public class DeviceSetMutedAction : IMassiveKnobAction
{
public Guid ActionId { get; } = new Guid("032eb405-a1df-4178-b2d5-6cf556305a8c");
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputDigital;
public string Name { get; } = Strings.SetMutedName;
public string Description { get; } = Strings.SetMutedDescription;
public IMassiveKnobActionInstance Create()
{
return new Instance();
}
private class Instance : IMassiveKnobDigitalAction
{
private IMassiveKnobActionContext actionContext;
private DeviceSetMutedActionSettings settings;
private IDevice playbackDevice;
public void Initialize(IMassiveKnobActionContext context)
{
actionContext = context;
settings = context.GetSettings<DeviceSetMutedActionSettings>();
ApplySettings();
}
public void Dispose()
{
}
private void ApplySettings()
{
var coreAudioController = CoreAudioControllerInstance.Acquire();
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
}
public UserControl CreateSettingsControl()
{
var viewModel = new DeviceSetMutedActionSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) =>
{
if (!viewModel.IsSettingsProperty(args.PropertyName))
return;
actionContext.SetSettings(settings);
ApplySettings();
};
return new DeviceSetMutedActionSettingsView(viewModel);
}
public async ValueTask DigitalChanged(bool on)
{
if (playbackDevice == null)
return;
if (settings.Toggle)
{
if (!on)
return;
await playbackDevice.SetMuteAsync(!playbackDevice.IsMuted);
}
else
await playbackDevice.SetMuteAsync(settings.SetInverted ? !on : on);
if (settings.OSD)
OSDManager.Show(playbackDevice);
}
}
}
}

View File

@ -0,0 +1,10 @@
using MassiveKnob.Plugin.CoreAudio.Base;
namespace MassiveKnob.Plugin.CoreAudio.SetMuted
{
public class DeviceSetMutedActionSettings : BaseDeviceSettings
{
public bool Toggle { get; set; }
public bool SetInverted { get; set;}
}
}

View File

@ -0,0 +1,14 @@
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.SetMuted.DeviceSetMutedActionSettingsView"
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:setMuted="clr-namespace:MassiveKnob.Plugin.CoreAudio.SetMuted"
xmlns:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance setMuted:DeviceSetMutedActionSettingsViewModel}">
<StackPanel Orientation="Vertical">
<base:BaseDeviceSettingsView />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,14 @@
namespace MassiveKnob.Plugin.CoreAudio.SetMuted
{
/// <summary>
/// Interaction logic for DeviceSetMutedActionSettingsView.xaml
/// </summary>
public partial class DeviceSetMutedActionSettingsView
{
public DeviceSetMutedActionSettingsView(DeviceSetMutedActionSettingsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,42 @@
using MassiveKnob.Plugin.CoreAudio.Base;
namespace MassiveKnob.Plugin.CoreAudio.SetMuted
{
public class DeviceSetMutedActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceSetMutedActionSettings>
{
// ReSharper disable UnusedMember.Global - used by WPF Binding
public bool Toggle
{
get => Settings.Toggle;
set
{
if (value == Settings.Toggle)
return;
Settings.Toggle = value;
OnPropertyChanged();
}
}
public bool SetInverted
{
get => Settings.SetInverted;
set
{
if (value == Settings.SetInverted)
return;
Settings.SetInverted = value;
OnPropertyChanged();
}
}
// ReSharper restore UnusedMember.Global
// ReSharper disable once SuggestBaseTypeForParameter - by design
public DeviceSetMutedActionSettingsViewModel(DeviceSetMutedActionSettings settings) : base(settings)
{
}
}
}

View File

@ -2,16 +2,16 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Controls; using System.Windows.Controls;
using AudioSwitcher.AudioApi; using AudioSwitcher.AudioApi;
using MassiveKnob.Plugin.CoreAudio.Settings; using MassiveKnob.Plugin.CoreAudio.OSD;
namespace MassiveKnob.Plugin.CoreAudio.Actions namespace MassiveKnob.Plugin.CoreAudio.SetVolume
{ {
public class DeviceVolumeAction : IMassiveKnobAction public class DeviceSetVolumeAction : IMassiveKnobAction
{ {
public Guid ActionId { get; } = new Guid("aabd329c-8be5-4d1e-90ab-5114143b21dd"); public Guid ActionId { get; } = new Guid("aabd329c-8be5-4d1e-90ab-5114143b21dd");
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputAnalog; public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputAnalog;
public string Name { get; } = "Set volume"; public string Name { get; } = Strings.SetVolumeName;
public string Description { get; } = "Sets the volume for the selected device, regardless of the current default device."; public string Description { get; } = Strings.SetVolumeDescription;
public IMassiveKnobActionInstance Create() public IMassiveKnobActionInstance Create()
@ -23,14 +23,14 @@ namespace MassiveKnob.Plugin.CoreAudio.Actions
private class Instance : IMassiveKnobAnalogAction private class Instance : IMassiveKnobAnalogAction
{ {
private IMassiveKnobActionContext actionContext; private IMassiveKnobActionContext actionContext;
private DeviceVolumeActionSettings settings; private DeviceSetVolumeActionSettings settings;
private IDevice playbackDevice; private IDevice playbackDevice;
public void Initialize(IMassiveKnobActionContext context) public void Initialize(IMassiveKnobActionContext context)
{ {
actionContext = context; actionContext = context;
settings = context.GetSettings<DeviceVolumeActionSettings>(); settings = context.GetSettings<DeviceSetVolumeActionSettings>();
ApplySettings(); ApplySettings();
} }
@ -49,7 +49,7 @@ namespace MassiveKnob.Plugin.CoreAudio.Actions
public UserControl CreateSettingsControl() public UserControl CreateSettingsControl()
{ {
var viewModel = new DeviceVolumeActionSettingsViewModel(settings); var viewModel = new DeviceSetVolumeActionSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) => viewModel.PropertyChanged += (sender, args) =>
{ {
if (!viewModel.IsSettingsProperty(args.PropertyName)) if (!viewModel.IsSettingsProperty(args.PropertyName))
@ -59,7 +59,7 @@ namespace MassiveKnob.Plugin.CoreAudio.Actions
ApplySettings(); ApplySettings();
}; };
return new DeviceVolumeActionSettingsView(viewModel); return new DeviceSetVolumeActionSettingsView(viewModel);
} }
@ -69,6 +69,9 @@ namespace MassiveKnob.Plugin.CoreAudio.Actions
return; return;
await playbackDevice.SetVolumeAsync(value); await playbackDevice.SetVolumeAsync(value);
if (settings.OSD)
OSDManager.Show(playbackDevice);
} }
} }
} }

View File

@ -0,0 +1,9 @@
using MassiveKnob.Plugin.CoreAudio.Base;
namespace MassiveKnob.Plugin.CoreAudio.SetVolume
{
public class DeviceSetVolumeActionSettings : BaseDeviceSettings
{
// TODO OSD
}
}

View File

@ -0,0 +1,14 @@
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.SetVolume.DeviceSetVolumeActionSettingsView"
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:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
xmlns:setVolume="clr-namespace:MassiveKnob.Plugin.CoreAudio.SetVolume"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance setVolume:DeviceSetVolumeActionSettingsViewModel}">
<StackPanel Orientation="Vertical">
<base:BaseDeviceSettingsView />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,14 @@
namespace MassiveKnob.Plugin.CoreAudio.SetVolume
{
/// <summary>
/// Interaction logic for DeviceSetVolumeActionSettingsView.xaml
/// </summary>
public partial class DeviceSetVolumeActionSettingsView
{
public DeviceSetVolumeActionSettingsView(DeviceSetVolumeActionSettingsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,12 @@
using MassiveKnob.Plugin.CoreAudio.Base;
namespace MassiveKnob.Plugin.CoreAudio.SetVolume
{
public class DeviceSetVolumeActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceSetVolumeActionSettings>
{
// ReSharper disable once SuggestBaseTypeForParameter - by design
public DeviceSetVolumeActionSettingsViewModel(DeviceSetVolumeActionSettings settings) : base(settings)
{
}
}
}

View File

@ -1,9 +0,0 @@
using System;
namespace MassiveKnob.Plugin.CoreAudio.Settings
{
public class BaseDeviceSettings
{
public Guid? DeviceId { get; set; }
}
}

View File

@ -1,7 +0,0 @@
namespace MassiveKnob.Plugin.CoreAudio.Settings
{
public class DeviceVolumeActionSettings : BaseDeviceSettings
{
// TODO OSD
}
}

View File

@ -1,14 +0,0 @@
namespace MassiveKnob.Plugin.CoreAudio.Settings
{
/// <summary>
/// Interaction logic for DeviceVolumeActionSettingsView.xaml
/// </summary>
public partial class DeviceVolumeActionSettingsView
{
public DeviceVolumeActionSettingsView(DeviceVolumeActionSettingsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -1,10 +0,0 @@
namespace MassiveKnob.Plugin.CoreAudio.Settings
{
public class DeviceVolumeActionSettingsViewModel : BaseDeviceSettingsViewModel
{
// ReSharper disable once SuggestBaseTypeForParameter - by design
public DeviceVolumeActionSettingsViewModel(DeviceVolumeActionSettings settings) : base(settings)
{
}
}
}

View File

@ -22,7 +22,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Strings { public class Strings {
private static global::System.Resources.ResourceManager resourceMan; private static global::System.Resources.ResourceManager resourceMan;
@ -36,7 +36,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// Returns the cached ResourceManager instance used by this class. /// Returns the cached ResourceManager instance used by this class.
/// </summary> /// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager { public static global::System.Resources.ResourceManager ResourceManager {
get { get {
if (object.ReferenceEquals(resourceMan, null)) { if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Plugin.CoreAudio.Strings", typeof(Strings).Assembly); global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Plugin.CoreAudio.Strings", typeof(Strings).Assembly);
@ -51,7 +51,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// resource lookups using this strongly typed resource class. /// resource lookups using this strongly typed resource class.
/// </summary> /// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture { public static global::System.Globalization.CultureInfo Culture {
get { get {
return resourceCulture; return resourceCulture;
} }
@ -63,7 +63,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary> /// <summary>
/// Looks up a localized string similar to {0}. /// Looks up a localized string similar to {0}.
/// </summary> /// </summary>
internal static string DeviceDisplayNameActive { public static string DeviceDisplayNameActive {
get { get {
return ResourceManager.GetString("DeviceDisplayNameActive", resourceCulture); return ResourceManager.GetString("DeviceDisplayNameActive", resourceCulture);
} }
@ -72,7 +72,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary> /// <summary>
/// Looks up a localized string similar to {0} (Disabled). /// Looks up a localized string similar to {0} (Disabled).
/// </summary> /// </summary>
internal static string DeviceDisplayNameDisabled { public static string DeviceDisplayNameDisabled {
get { get {
return ResourceManager.GetString("DeviceDisplayNameDisabled", resourceCulture); return ResourceManager.GetString("DeviceDisplayNameDisabled", resourceCulture);
} }
@ -81,7 +81,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary> /// <summary>
/// Looks up a localized string similar to {0} (Inactive). /// Looks up a localized string similar to {0} (Inactive).
/// </summary> /// </summary>
internal static string DeviceDisplayNameInactive { public static string DeviceDisplayNameInactive {
get { get {
return ResourceManager.GetString("DeviceDisplayNameInactive", resourceCulture); return ResourceManager.GetString("DeviceDisplayNameInactive", resourceCulture);
} }
@ -90,7 +90,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary> /// <summary>
/// Looks up a localized string similar to {0} (Not present). /// Looks up a localized string similar to {0} (Not present).
/// </summary> /// </summary>
internal static string DeviceDisplayNameNotPresent { public static string DeviceDisplayNameNotPresent {
get { get {
return ResourceManager.GetString("DeviceDisplayNameNotPresent", resourceCulture); return ResourceManager.GetString("DeviceDisplayNameNotPresent", resourceCulture);
} }
@ -99,10 +99,154 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary> /// <summary>
/// Looks up a localized string similar to {0} (Unplugged). /// Looks up a localized string similar to {0} (Unplugged).
/// </summary> /// </summary>
internal static string DeviceDisplayNameUnplugged { public static string DeviceDisplayNameUnplugged {
get { get {
return ResourceManager.GetString("DeviceDisplayNameUnplugged", resourceCulture); return ResourceManager.GetString("DeviceDisplayNameUnplugged", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Sets the digital output to the muted state for the selected device, regardless of the current default device..
/// </summary>
public static string GetMutedDescription {
get {
return ResourceManager.GetString("GetMutedDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Muted / unmuted.
/// </summary>
public static string GetMutedName {
get {
return ResourceManager.GetString("GetMutedName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Sets the analog output to the volume for the selected device, regardless of the current default device..
/// </summary>
public static string GetVolumeDescription {
get {
return ResourceManager.GetString("GetVolumeDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Volume.
/// </summary>
public static string GetVolumeName {
get {
return ResourceManager.GetString("GetVolumeName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Included with Massive Knob by default. Provides volume control per device and default device switching..
/// </summary>
public static string PluginDescription {
get {
return ResourceManager.GetString("PluginDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Windows Core Audio.
/// </summary>
public static string PluginName {
get {
return ResourceManager.GetString("PluginName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Toggles the muted state for the selected device, regardless of the current default device..
/// </summary>
public static string SetMutedDescription {
get {
return ResourceManager.GetString("SetMutedDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Mute / unmute.
/// </summary>
public static string SetMutedName {
get {
return ResourceManager.GetString("SetMutedName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Inverted (muted is off).
/// </summary>
public static string SettingGetMutedInverted {
get {
return ResourceManager.GetString("SettingGetMutedInverted", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show On-Screen Display.
/// </summary>
public static string SettingOSD {
get {
return ResourceManager.GetString("SettingOSD", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Playback device.
/// </summary>
public static string SettingPlaybackDevice {
get {
return ResourceManager.GetString("SettingPlaybackDevice", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Inverted (off is muted).
/// </summary>
public static string SettingSetMutedSetInverted {
get {
return ResourceManager.GetString("SettingSetMutedSetInverted", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Set mute depending on value (eg. switch).
/// </summary>
public static string SettingSetMutedToggleFalse {
get {
return ResourceManager.GetString("SettingSetMutedToggleFalse", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Toggle mute when input turns on (eg. push button).
/// </summary>
public static string SettingSetMutedToggleTrue {
get {
return ResourceManager.GetString("SettingSetMutedToggleTrue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Sets the volume for the selected device to the value of the analog input, regardless of the current default device..
/// </summary>
public static string SetVolumeDescription {
get {
return ResourceManager.GetString("SetVolumeDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Volume.
/// </summary>
public static string SetVolumeName {
get {
return ResourceManager.GetString("SetVolumeName", resourceCulture);
}
}
} }
} }

View File

@ -132,4 +132,52 @@
<data name="DeviceDisplayNameUnplugged" xml:space="preserve"> <data name="DeviceDisplayNameUnplugged" xml:space="preserve">
<value>{0} (Unplugged)</value> <value>{0} (Unplugged)</value>
</data> </data>
<data name="GetMutedDescription" xml:space="preserve">
<value>Sets the digital output to the muted state for the selected device, regardless of the current default device.</value>
</data>
<data name="GetMutedName" xml:space="preserve">
<value>Muted / unmuted</value>
</data>
<data name="GetVolumeDescription" xml:space="preserve">
<value>Sets the analog output to the volume for the selected device, regardless of the current default device.</value>
</data>
<data name="GetVolumeName" xml:space="preserve">
<value>Volume</value>
</data>
<data name="PluginDescription" xml:space="preserve">
<value>Included with Massive Knob by default. Provides volume control per device and default device switching.</value>
</data>
<data name="PluginName" xml:space="preserve">
<value>Windows Core Audio</value>
</data>
<data name="SetMutedDescription" xml:space="preserve">
<value>Toggles the muted state for the selected device, regardless of the current default device.</value>
</data>
<data name="SetMutedName" xml:space="preserve">
<value>Mute / unmute</value>
</data>
<data name="SettingGetMutedInverted" xml:space="preserve">
<value>Inverted (muted is off)</value>
</data>
<data name="SettingOSD" xml:space="preserve">
<value>Show On-Screen Display</value>
</data>
<data name="SettingPlaybackDevice" xml:space="preserve">
<value>Playback device</value>
</data>
<data name="SettingSetMutedSetInverted" xml:space="preserve">
<value>Inverted (off is muted)</value>
</data>
<data name="SettingSetMutedToggleFalse" xml:space="preserve">
<value>Set mute depending on value (eg. switch)</value>
</data>
<data name="SettingSetMutedToggleTrue" xml:space="preserve">
<value>Toggle mute when input turns on (eg. push button)</value>
</data>
<data name="SetVolumeDescription" xml:space="preserve">
<value>Sets the volume for the selected device to the value of the analog input, regardless of the current default device.</value>
</data>
<data name="SetVolumeName" xml:space="preserve">
<value>Volume</value>
</data>
</root> </root>

View File

@ -0,0 +1,97 @@
using System;
using System.Threading;
using System.Windows.Controls;
using MassiveKnob.Plugin.EmulatorDevice.Settings;
namespace MassiveKnob.Plugin.EmulatorDevice.Devices
{
public class EmulatorDevice : IMassiveKnobDevice
{
public Guid DeviceId { get; } = new Guid("e1a4977a-abf4-4c75-a17d-fd8d3a8451ff");
public string Name { get; } = "Mock device";
public string Description { get; } = "Emulates the actual device but does not communicate with anything.";
public IMassiveKnobDeviceInstance Create()
{
return new Instance();
}
private class Instance : IMassiveKnobDeviceInstance
{
private IMassiveKnobDeviceContext deviceContext;
private EmulatorDeviceSettings settings;
private DeviceSpecs reportedSpecs;
private EmulatorDeviceWindow window;
private EmulatorDeviceWindowViewModel windowViewModel;
public void Initialize(IMassiveKnobDeviceContext context)
{
deviceContext = context;
settings = deviceContext.GetSettings<EmulatorDeviceSettings>();
windowViewModel = new EmulatorDeviceWindowViewModel(settings, context);
window = new EmulatorDeviceWindow(windowViewModel);
ApplySettings();
}
public void Dispose()
{
window.Close();
}
private void ApplySettings()
{
if (settings.AnalogInputCount != reportedSpecs.AnalogInputCount ||
settings.DigitalInputCount != reportedSpecs.DigitalInputCount ||
settings.AnalogOutputCount != reportedSpecs.AnalogOutputCount ||
settings.DigitalOutputCount != reportedSpecs.DigitalOutputCount)
{
reportedSpecs = new DeviceSpecs(
settings.AnalogInputCount, settings.DigitalInputCount,
settings.AnalogOutputCount, settings.DigitalOutputCount);
deviceContext.Connected(reportedSpecs);
}
windowViewModel.ApplySettings();
window.Show();
}
public UserControl CreateSettingsControl()
{
var viewModel = new EmulatorDeviceSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) =>
{
deviceContext.SetSettings(settings);
ApplySettings();
};
return new EmulatorDeviceSettingsView(viewModel);
}
public void SetAnalogOutput(int analogOutputIndex, byte value)
{
if (analogOutputIndex >= windowViewModel.AnalogOutputCount)
return;
windowViewModel.AnalogOutputs[analogOutputIndex].AnalogValue = value;
}
public void SetDigitalOutput(int digitalOutputIndex, bool @on)
{
if (digitalOutputIndex >= windowViewModel.DigitalOutputCount)
return;
windowViewModel.DigitalOutputs[digitalOutputIndex].DigitalValue = on;
}
}
}
}

View File

@ -0,0 +1,75 @@
<Window x:Class="MassiveKnob.Plugin.EmulatorDevice.Devices.EmulatorDeviceWindow"
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"
xmlns:devices="clr-namespace:MassiveKnob.Plugin.EmulatorDevice.Devices"
mc:Ignorable="d"
Title="Massive Knob - Device Emulator" Height="400" Width="300"
WindowStartupLocation="CenterScreen"
WindowStyle="ToolWindow"
Topmost="True"
d:DataContext="{d:DesignInstance devices:EmulatorDeviceWindowViewModelDesignTime, IsDesignTimeCreatable=True}">
<Window.Resources>
<ResourceDictionary>
<Style TargetType="DockPanel" x:Key="Row">
</Style>
<Style TargetType="TextBlock" x:Key="Label">
<Setter Property="Margin" Value="4,4,8,4" />
<Setter Property="DockPanel.Dock" Value="Left" />
</Style>
<Style x:Key="Value">
<Setter Property="Control.Margin" Value="4,4,8,4" />
</Style>
</ResourceDictionary>
</Window.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<ItemsControl ItemsSource="{Binding AnalogInputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel Style="{StaticResource Row}">
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource Label}" />
<Slider Minimum="0" Maximum="100" Value="{Binding AnalogValue}" Style="{StaticResource Value}" />
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding DigitalInputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel Style="{StaticResource Row}">
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource Label}" />
<CheckBox IsChecked="{Binding DigitalValue}" Style="{StaticResource Value}" />
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding AnalogOutputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel Style="{StaticResource Row}">
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource Label}" />
<TextBlock Text="{Binding AnalogValue}" Style="{StaticResource Value}" />
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding DigitalOutputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel Style="{StaticResource Row}">
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource Label}" />
<TextBlock Text="{Binding DigitalValueDisplayText}" Style="{StaticResource Value}" />
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Window>

View File

@ -0,0 +1,14 @@
namespace MassiveKnob.Plugin.EmulatorDevice.Devices
{
/// <summary>
/// Interaction logic for EmulatorDeviceWindow.xaml
/// </summary>
public partial class EmulatorDeviceWindow
{
public EmulatorDeviceWindow(EmulatorDeviceWindowViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using MassiveKnob.Plugin.EmulatorDevice.Settings;
namespace MassiveKnob.Plugin.EmulatorDevice.Devices
{
public class EmulatorDeviceWindowViewModel : INotifyPropertyChanged
{
private readonly EmulatorDeviceSettings settings;
private readonly IMassiveKnobDeviceContext context;
public event PropertyChangedEventHandler PropertyChanged;
// ReSharper disable UnusedMember.Global - used by WPF Binding
private int analogInputCount;
public int AnalogInputCount
{
get => analogInputCount;
set
{
if (value == analogInputCount)
return;
analogInputCount = value;
OnPropertyChanged();
AnalogInputs = Enumerable.Range(0, AnalogInputCount)
.Select(i => new InputOutputViewModel(context, MassiveKnobActionType.InputAnalog, i))
.ToList();
}
}
private IList<InputOutputViewModel> analogInputs;
public IList<InputOutputViewModel> AnalogInputs
{
get => analogInputs;
set
{
analogInputs = value;
OnPropertyChanged();
}
}
private int digitalInputCount;
public int DigitalInputCount
{
get => digitalInputCount;
set
{
if (value == digitalInputCount)
return;
digitalInputCount = value;
OnPropertyChanged();
DigitalInputs = Enumerable.Range(0, DigitalInputCount)
.Select(i => new InputOutputViewModel(context, MassiveKnobActionType.InputDigital, i))
.ToList();
}
}
private IList<InputOutputViewModel> digitalInputs;
public IList<InputOutputViewModel> DigitalInputs
{
get => digitalInputs;
set
{
digitalInputs = value;
OnPropertyChanged();
}
}
private int analogOutputCount;
public int AnalogOutputCount
{
get => analogOutputCount;
set
{
if (value == analogOutputCount)
return;
analogOutputCount = value;
OnPropertyChanged();
AnalogOutputs = Enumerable.Range(0, AnalogOutputCount)
.Select(i => new InputOutputViewModel(context, MassiveKnobActionType.OutputAnalog, i))
.ToList();
}
}
private IList<InputOutputViewModel> analogOutputs;
public IList<InputOutputViewModel> AnalogOutputs
{
get => analogOutputs;
set
{
analogOutputs = value;
OnPropertyChanged();
}
}
private int digitalOutputCount;
public int DigitalOutputCount
{
get => digitalOutputCount;
set
{
if (value == digitalOutputCount)
return;
digitalOutputCount = value;
OnPropertyChanged();
DigitalOutputs = Enumerable.Range(0, DigitalOutputCount)
.Select(i => new InputOutputViewModel(context, MassiveKnobActionType.OutputDigital, i))
.ToList();
}
}
private IList<InputOutputViewModel> digitalOutputs;
public IList<InputOutputViewModel> DigitalOutputs
{
get => digitalOutputs;
set
{
digitalOutputs = value;
OnPropertyChanged();
}
}
// ReSharper restore UnusedMember.Global
public EmulatorDeviceWindowViewModel(EmulatorDeviceSettings settings, IMassiveKnobDeviceContext context)
{
this.settings = settings;
this.context = context;
ApplySettings();
}
public void ApplySettings()
{
AnalogInputCount = settings.AnalogInputCount;
DigitalInputCount = settings.DigitalInputCount;
AnalogOutputCount = settings.AnalogOutputCount;
DigitalOutputCount = settings.DigitalOutputCount;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public class InputOutputViewModel : INotifyPropertyChanged
{
private readonly IMassiveKnobDeviceContext context;
public MassiveKnobActionType ActionType { get; }
public int Index { get; }
public string DisplayName
{
get
{
switch (ActionType)
{
case MassiveKnobActionType.InputAnalog:
return $"Analog input #{Index + 1}";
case MassiveKnobActionType.InputDigital:
return $"Digital input #{Index + 1}";
case MassiveKnobActionType.OutputAnalog:
return $"Analog output #{Index + 1}";
case MassiveKnobActionType.OutputDigital:
return $"Digital output #{Index + 1}";
default:
return (Index + 1).ToString();
}
}
}
private byte analogValue;
public byte AnalogValue
{
get => analogValue;
set
{
analogValue = value;
OnPropertyChanged();
if (ActionType == MassiveKnobActionType.InputAnalog)
// Context can be null in DesignTime
context?.AnalogChanged(Index, analogValue);
}
}
private bool digitalValue;
public bool DigitalValue
{
get => digitalValue;
set
{
digitalValue = value;
OnPropertyChanged();
OnDependantPropertyChanged("DigitalValueDisplayText");
if (ActionType == MassiveKnobActionType.InputDigital)
context?.DigitalChanged(Index, digitalValue);
}
}
public string DigitalValueDisplayText => DigitalValue ? "On" : "Off";
public InputOutputViewModel(IMassiveKnobDeviceContext context, MassiveKnobActionType actionType, int index)
{
this.context = context;
ActionType = actionType;
Index = index;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void OnDependantPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class EmulatorDeviceWindowViewModelDesignTime : EmulatorDeviceWindowViewModel
{
public EmulatorDeviceWindowViewModelDesignTime() : base(
new EmulatorDeviceSettings
{
AnalogInputCount = 2,
DigitalInputCount = 2,
AnalogOutputCount = 2,
DigitalOutputCount = 2
}, null)
{
}
}
}

View File

@ -7,8 +7,8 @@
<ProjectGuid>{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}</ProjectGuid> <ProjectGuid>{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}</ProjectGuid>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MassiveKnob.Plugin.MockDevice</RootNamespace> <RootNamespace>MassiveKnob.Plugin.EmulatorDevice</RootNamespace>
<AssemblyName>MassiveKnob.Plugin.MockDevice</AssemblyName> <AssemblyName>MassiveKnob.Plugin.EmulatorDevice</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
@ -45,14 +45,18 @@
<Reference Include="WindowsBase" /> <Reference Include="WindowsBase" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Devices\MockDevice.cs" /> <Compile Include="Devices\EmulatorDevice.cs" />
<Compile Include="MassiveKnobMockDevicePlugin.cs" /> <Compile Include="Devices\EmulatorDeviceWindow.xaml.cs">
<Compile Include="Properties\AssemblyInfo.cs" /> <DependentUpon>EmulatorDeviceWindow.xaml</DependentUpon>
<Compile Include="Settings\MockDeviceSettingsView.xaml.cs">
<DependentUpon>MockDeviceSettingsView.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Settings\MockDeviceSettings.cs" /> <Compile Include="Devices\EmulatorDeviceWindowViewModel.cs" />
<Compile Include="Settings\MockDeviceSettingsViewModel.cs" /> <Compile Include="MassiveKnobEmulatorDevicePlugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Settings\EmulatorDeviceSettingsView.xaml.cs">
<DependentUpon>EmulatorDeviceSettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="Settings\EmulatorDeviceSettings.cs" />
<Compile Include="Settings\EmulatorDeviceSettingsViewModel.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj"> <ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
@ -62,7 +66,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup />
<ItemGroup> <ItemGroup>
<Page Include="Settings\MockDeviceSettingsView.xaml"> <Page Include="Devices\EmulatorDeviceWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Settings\EmulatorDeviceSettingsView.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>

View File

@ -1,10 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace MassiveKnob.Plugin.MockDevice namespace MassiveKnob.Plugin.EmulatorDevice
{ {
[MassiveKnobPlugin] [MassiveKnobPlugin]
public class MassiveKnobMockDevicePlugin : IMassiveKnobDevicePlugin public class MassiveKnobEmulatorDevicePlugin : IMassiveKnobDevicePlugin
{ {
public Guid PluginId { get; } = new Guid("85f04232-d70f-494c-94a2-41452591ffb3"); public Guid PluginId { get; } = new Guid("85f04232-d70f-494c-94a2-41452591ffb3");
public string Name { get; } = "Mock Device"; public string Name { get; } = "Mock Device";
@ -14,7 +14,7 @@ namespace MassiveKnob.Plugin.MockDevice
public IEnumerable<IMassiveKnobDevice> Devices { get; } = new IMassiveKnobDevice[] public IEnumerable<IMassiveKnobDevice> Devices { get; } = new IMassiveKnobDevice[]
{ {
new Devices.MockDevice() new EmulatorDevice.Devices.EmulatorDevice()
}; };
} }
} }

View File

@ -0,0 +1,10 @@
namespace MassiveKnob.Plugin.EmulatorDevice.Settings
{
public class EmulatorDeviceSettings
{
public int AnalogInputCount { get; set; } = 2;
public int DigitalInputCount { get; set; } = 2;
public int AnalogOutputCount { get; set; } = 2;
public int DigitalOutputCount { get; set; } = 2;
}
}

View File

@ -0,0 +1,34 @@
<UserControl x:Class="MassiveKnob.Plugin.EmulatorDevice.Settings.EmulatorDeviceSettingsView"
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.Plugin.EmulatorDevice.Settings"
mc:Ignorable="d" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=settings:EmulatorDeviceSettingsViewModel}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="4">Analog inputs</TextBlock>
<TextBox Grid.Row="0" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding AnalogInputCount}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="4">Digital inputs</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding DigitalInputCount}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="4">Analog outputs</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding AnalogOutputCount}" />
<TextBlock Grid.Row="3" Grid.Column="0" Margin="4">Digital outputs</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding DigitalOutputCount}" />
</Grid>
</UserControl>

View File

@ -0,0 +1,15 @@
namespace MassiveKnob.Plugin.EmulatorDevice.Settings
{
/// <summary>
/// Interaction logic for EmulatorDeviceSettingsView.xaml
/// </summary>
public partial class EmulatorDeviceSettingsView
{
public EmulatorDeviceSettingsView(EmulatorDeviceSettingsViewModel settingsViewModel)
{
DataContext = settingsViewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,81 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MassiveKnob.Plugin.EmulatorDevice.Settings
{
public class EmulatorDeviceSettingsViewModel : INotifyPropertyChanged
{
private readonly EmulatorDeviceSettings settings;
public event PropertyChangedEventHandler PropertyChanged;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public int AnalogInputCount
{
get => settings.AnalogInputCount;
set
{
if (value == settings.AnalogInputCount)
return;
settings.AnalogInputCount = value;
OnPropertyChanged();
}
}
public int DigitalInputCount
{
get => settings.DigitalInputCount;
set
{
if (value == settings.DigitalInputCount)
return;
settings.DigitalInputCount = value;
OnPropertyChanged();
}
}
public int AnalogOutputCount
{
get => settings.AnalogOutputCount;
set
{
if (value == settings.AnalogOutputCount)
return;
settings.AnalogOutputCount = value;
OnPropertyChanged();
}
}
public int DigitalOutputCount
{
get => settings.DigitalOutputCount;
set
{
if (value == settings.DigitalOutputCount)
return;
settings.DigitalOutputCount = value;
OnPropertyChanged();
}
}
// ReSharper restore UnusedMember.Global
public EmulatorDeviceSettingsViewModel(EmulatorDeviceSettings settings)
{
this.settings = settings;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -1,95 +0,0 @@
using System;
using System.Threading;
using System.Windows.Controls;
using MassiveKnob.Plugin.MockDevice.Settings;
namespace MassiveKnob.Plugin.MockDevice.Devices
{
public class MockDevice : IMassiveKnobDevice
{
public Guid DeviceId { get; } = new Guid("e1a4977a-abf4-4c75-a17d-fd8d3a8451ff");
public string Name { get; } = "Mock device";
public string Description { get; } = "Emulates the actual device but does not communicate with anything.";
public IMassiveKnobDeviceInstance Create()
{
return new Instance();
}
private class Instance : IMassiveKnobDeviceInstance
{
private IMassiveKnobDeviceContext deviceContext;
private MockDeviceSettings settings;
private Timer inputChangeTimer;
private int reportedAnalogInputCount;
private int reportedDigitalInputCount;
private readonly Random random = new Random();
public void Initialize(IMassiveKnobDeviceContext context)
{
deviceContext = context;
settings = deviceContext.GetSettings<MockDeviceSettings>();
ApplySettings();
}
public void Dispose()
{
inputChangeTimer?.Dispose();
}
private void ApplySettings()
{
if (settings.AnalogCount != reportedAnalogInputCount ||
settings.DigitalCount != reportedDigitalInputCount)
{
deviceContext.Connected(new DeviceSpecs(settings.AnalogCount, settings.DigitalCount, 0, 0));
reportedAnalogInputCount = settings.AnalogCount;
reportedDigitalInputCount = settings.DigitalCount;
}
var interval = TimeSpan.FromSeconds(Math.Max(settings.Interval, 1));
if (inputChangeTimer == null)
inputChangeTimer = new Timer(Tick, null, interval, interval);
else
inputChangeTimer.Change(interval, interval);
}
public UserControl CreateSettingsControl()
{
var viewModel = new MockDeviceSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) =>
{
deviceContext.SetSettings(settings);
ApplySettings();
};
return new MockDeviceSettingsView(viewModel);
}
private void Tick(object state)
{
var totalInputCount = reportedAnalogInputCount + reportedDigitalInputCount;
if (totalInputCount == 0)
return;
var changeInput = random.Next(0, totalInputCount);
if (changeInput < reportedAnalogInputCount)
deviceContext.AnalogChanged(changeInput, (byte)random.Next(0, 101));
else
deviceContext.DigitalChanged(changeInput - reportedAnalogInputCount, random.Next(0, 2) == 1);
}
}
}
}

View File

@ -1,9 +0,0 @@
namespace MassiveKnob.Plugin.MockDevice.Settings
{
public class MockDeviceSettings
{
public int AnalogCount { get; set; } = 3;
public int DigitalCount { get; set; } = 1;
public int Interval { get; set; } = 5;
}
}

View File

@ -1,30 +0,0 @@
<UserControl x:Class="MassiveKnob.Plugin.MockDevice.Settings.MockDeviceSettingsView"
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.Plugin.MockDevice.Settings"
mc:Ignorable="d" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=settings:MockDeviceSettingsViewModel}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="4">Number of analog inputs</TextBlock>
<TextBox Grid.Row="0" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding AnalogCount}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="4">Number of digital inputs</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding DigitalCount}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="4">Random change interval (seconds)</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding Interval}" />
</Grid>
</UserControl>

View File

@ -1,15 +0,0 @@
namespace MassiveKnob.Plugin.MockDevice.Settings
{
/// <summary>
/// Interaction logic for MockDeviceSettingsView.xaml
/// </summary>
public partial class MockDeviceSettingsView
{
public MockDeviceSettingsView(MockDeviceSettingsViewModel settingsViewModel)
{
DataContext = settingsViewModel;
InitializeComponent();
}
}
}

View File

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

View File

@ -59,6 +59,17 @@ namespace MassiveKnob.Plugin.SerialDevice.Devices
return new SerialDeviceSettingsView(viewModel); return new SerialDeviceSettingsView(viewModel);
} }
public void SetAnalogOutput(int analogOutputIndex, byte value)
{
// TODO Support SetAnalogOutput
}
public void SetDigitalOutput(int digitalOutputIndex, bool @on)
{
// TODO Support SetDigitalOutput
}
} }
} }
} }

View File

@ -174,6 +174,7 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
if ((char) response == 'H') if ((char) response == 'H')
{ {
// TODO support multiple I/O's
var knobCount = serialPort.ReadByte(); var knobCount = serialPort.ReadByte();
if (knobCount > -1) if (knobCount > -1)
{ {

View File

@ -19,5 +19,19 @@ namespace MassiveKnob.Plugin
/// determined by the UserControl. Return null to indicate there are no settings for this device. /// determined by the UserControl. Return null to indicate there are no settings for this device.
/// </summary> /// </summary>
UserControl CreateSettingsControl(); UserControl CreateSettingsControl();
/// <summary>
/// Called when the state of an analog output should be changed.
/// </summary>
/// <param name="analogOutputIndex">The index of the analog output to set.</param>
/// <param name="value">The analog value in the range of 0 to 100.</param>
void SetAnalogOutput(int analogOutputIndex, byte value);
/// <summary>
/// Called when the state of a digital output should be changed.
/// </summary>
/// <param name="digitalOutputIndex">The index of the digital output to set.</param>
/// <param name="on">Whether the signal is on or off.</param>
void SetDigitalOutput(int digitalOutputIndex, bool on);
} }
} }

View File

@ -9,7 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin", "Massi
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.CoreAudio", "MassiveKnob.Plugin.CoreAudio\MassiveKnob.Plugin.CoreAudio.csproj", "{5BD5E2F2-9923-4F74-AC69-ACDA0B847937}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.CoreAudio", "MassiveKnob.Plugin.CoreAudio\MassiveKnob.Plugin.CoreAudio.csproj", "{5BD5E2F2-9923-4F74-AC69-ACDA0B847937}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.MockDevice", "MassiveKnob.Plugin.MockDevice\MassiveKnob.Plugin.MockDevice.csproj", "{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.EmulatorDevice", "MassiveKnob.Plugin.EmulatorDevice\MassiveKnob.Plugin.EmulatorDevice.csproj", "{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.SerialDevice", "MassiveKnob.Plugin.SerialDevice\MassiveKnob.Plugin.SerialDevice.csproj", "{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.SerialDevice", "MassiveKnob.Plugin.SerialDevice\MassiveKnob.Plugin.SerialDevice.csproj", "{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}"
EndProject EndProject

View File

@ -1,2 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OSD/@EntryIndexedValue">OSD</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/Abbreviations/=OSD/@EntryIndexedValue">OSD</s:String></wpf:ResourceDictionary>

View File

@ -97,7 +97,7 @@ namespace MassiveKnob.Model
public MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index) public MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index)
{ {
var list = GetActionMappingList(actionType); var list = GetActionMappingList(actionType);
return index >= list.Count ? null : list[index].ActionInfo; return index >= list.Count ? null : list[index]?.ActionInfo;
} }
@ -244,7 +244,7 @@ namespace MassiveKnob.Model
if (index >= list.Count) if (index >= list.Count)
return; return;
if (list[index].Context != context) if (list[index]?.Context != context)
throw new InvalidOperationException("Caller must be the active action to retrieve the settings"); throw new InvalidOperationException("Caller must be the active action to retrieve the settings");
lock (settingsLock) lock (settingsLock)
@ -273,7 +273,7 @@ namespace MassiveKnob.Model
return; return;
var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog); var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog);
if (mapping == null || analogInputIndex >= mapping.Count) if (mapping == null || analogInputIndex >= mapping.Count || mapping[analogInputIndex] == null)
return; return;
if (mapping[analogInputIndex].ActionInfo.Instance is IMassiveKnobAnalogAction analogAction) if (mapping[analogInputIndex].ActionInfo.Instance is IMassiveKnobAnalogAction analogAction)
@ -286,8 +286,8 @@ namespace MassiveKnob.Model
if (context != activeDeviceContext) if (context != activeDeviceContext)
return; return;
var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog); var mapping = GetActionMappingList(MassiveKnobActionType.InputDigital);
if (mapping == null || digitalInputIndex >= mapping.Count) if (mapping == null || digitalInputIndex >= mapping.Count || mapping[digitalInputIndex] == null)
return; return;
if (mapping[digitalInputIndex].ActionInfo.Instance is IMassiveKnobDigitalAction digitalAction) if (mapping[digitalInputIndex].ActionInfo.Instance is IMassiveKnobDigitalAction digitalAction)
@ -295,6 +295,38 @@ namespace MassiveKnob.Model
} }
public void SetAnalogOutput(IMassiveKnobActionContext context, IMassiveKnobAction action, int index, byte value)
{
if (activeDevice == null)
return;
var list = GetActionMappingList(action.ActionType);
if (index >= list.Count)
return;
if (list[index]?.Context != context)
return;
activeDevice.Instance.SetAnalogOutput(index, value);
}
public void SetDigitalOutput(IMassiveKnobActionContext context, IMassiveKnobAction action, int index, bool on)
{
if (activeDevice == null)
return;
var list = GetActionMappingList(action.ActionType);
if (index >= list.Count)
return;
if (list[index]?.Context != context)
return;
activeDevice.Instance.SetDigitalOutput(index, on);
}
private List<ActionMapping> GetActionMappingList(MassiveKnobActionType actionType) private List<ActionMapping> GetActionMappingList(MassiveKnobActionType actionType)
{ {
switch (actionType) switch (actionType)
@ -534,15 +566,15 @@ namespace MassiveKnob.Model
} }
public void SetDigitalOutput(bool on)
{
throw new NotImplementedException();
}
public void SetAnalogOutput(byte value) public void SetAnalogOutput(byte value)
{ {
throw new NotImplementedException(); owner.SetAnalogOutput(this, action, index, value);
}
public void SetDigitalOutput(bool on)
{
owner.SetDigitalOutput(this, action, index, on);
} }
} }
} }

View File

@ -17,10 +17,9 @@
</Style> </Style>
<Style TargetType="TextBlock" x:Key="SubHeader"> <Style TargetType="TextBlock" x:Key="SubHeader">
<Setter Property="Background" Value="LightSlateGray" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" /> <Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="5" /> <Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Margin" Value="0,0,0,4" />
</Style> </Style>
<Style TargetType="StackPanel" x:Key="Content"> <Style TargetType="StackPanel" x:Key="Content">

View File

@ -26,10 +26,10 @@
</DataTemplate> </DataTemplate>
</ResourceDictionary> </ResourceDictionary>
</UserControl.Resources> </UserControl.Resources>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical" Style="{StaticResource Content}">
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource SubHeader}"></TextBlock> <TextBlock Text="{Binding DisplayName}" Style="{StaticResource SubHeader}"></TextBlock>
<StackPanel Orientation="Vertical" Style="{StaticResource Content}"> <StackPanel Orientation="Vertical">
<ComboBox <ComboBox
ItemsSource="{Binding Actions}" ItemsSource="{Binding Actions}"
SelectedItem="{Binding SelectedAction}" SelectedItem="{Binding SelectedAction}"

View File

@ -30,7 +30,7 @@
</ResourceDictionary> </ResourceDictionary>
</Window.Resources> </Window.Resources>
<ScrollViewer> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<!-- <!--

View File

@ -24,7 +24,7 @@ namespace MassiveKnob.ViewModel
? $"Output #{index + 1}" ? $"Output #{index + 1}"
: $"Input #{index + 1}"; : $"Input #{index + 1}";
public IList<ActionViewModel> Actions => settingsViewModel.Actions; public IList<ActionViewModel> Actions { get; }
public ActionViewModel SelectedAction public ActionViewModel SelectedAction
@ -66,6 +66,9 @@ namespace MassiveKnob.ViewModel
this.actionType = actionType; this.actionType = actionType;
this.index = index; this.index = index;
Actions = settingsViewModel.Actions.Where(a => a.RepresentsNull || a.Action.ActionType == actionType).ToList();
var actionInfo = orchestrator.GetAction(actionType, index); var actionInfo = orchestrator.GetAction(actionType, index);
selectedAction = actionInfo != null selectedAction = actionInfo != null

View File

@ -10,6 +10,7 @@ using MassiveKnob.Plugin;
namespace MassiveKnob.ViewModel namespace MassiveKnob.ViewModel
{ {
// TODO better design-time version
public class SettingsViewModel : INotifyPropertyChanged public class SettingsViewModel : INotifyPropertyChanged
{ {
private readonly IMassiveKnobOrchestrator orchestrator; private readonly IMassiveKnobOrchestrator orchestrator;
@ -65,8 +66,10 @@ namespace MassiveKnob.ViewModel
{ {
specs = value; specs = value;
OnPropertyChanged(); OnPropertyChanged();
OnOtherPropertyChanged("AnalogInputVisibility"); OnDependantPropertyChanged("AnalogInputVisibility");
OnOtherPropertyChanged("DigitalInputVisibility"); OnDependantPropertyChanged("DigitalInputVisibility");
OnDependantPropertyChanged("AnalogOutputVisibility");
OnDependantPropertyChanged("DigitalOutputVisibility");
AnalogInputs = Enumerable AnalogInputs = Enumerable
.Range(0, specs?.AnalogInputCount ?? 0) .Range(0, specs?.AnalogInputCount ?? 0)
@ -75,6 +78,14 @@ namespace MassiveKnob.ViewModel
DigitalInputs = Enumerable DigitalInputs = Enumerable
.Range(0, specs?.DigitalInputCount ?? 0) .Range(0, specs?.DigitalInputCount ?? 0)
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.InputDigital, i)); .Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.InputDigital, i));
AnalogOutputs = Enumerable
.Range(0, specs?.AnalogOutputCount ?? 0)
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.OutputAnalog, i));
DigitalOutputs = Enumerable
.Range(0, specs?.DigitalOutputCount ?? 0)
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.OutputDigital, i));
} }
} }
@ -175,7 +186,7 @@ namespace MassiveKnob.ViewModel
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
protected virtual void OnOtherPropertyChanged(string propertyName) protected virtual void OnDependantPropertyChanged(string propertyName)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }