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 running a VoiceMeeter macro
2. Corresponding LEDs to indicate the currently active device
3. OSD
4. API / plugins to use extra knobs and buttons for other purposes
3. OSD
4. API / plugins to use extra knobs and buttons for other purposes
## Developing
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 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
{
private readonly BaseDeviceSettings settings;
protected readonly BaseDeviceSettings Settings;
public event PropertyChangedEventHandler PropertyChanged;
private IList<PlaybackDeviceViewModel> playbackDevices;
@ -37,7 +48,21 @@ namespace MassiveKnob.Plugin.CoreAudio.Settings
return;
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();
}
}
@ -46,7 +71,7 @@ namespace MassiveKnob.Plugin.CoreAudio.Settings
public BaseDeviceSettingsViewModel(BaseDeviceSettings settings)
{
this.settings = settings;
Settings = settings;
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: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.CoreAudio.Settings"
xmlns:getMuted="clr-namespace:MassiveKnob.Plugin.CoreAudio.GetMuted"
xmlns:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance settings:DeviceVolumeActionSettingsViewModel}">
d:DataContext="{d:DesignInstance getMuted:DeviceGetMutedActionSettingsViewModel}">
<StackPanel Orientation="Vertical">
<TextBlock>Playback device</TextBlock>
<ComboBox ItemsSource="{Binding PlaybackDevices}" SelectedItem="{Binding SelectedDevice}" DisplayMemberPath="DisplayName" />
<base:BaseDeviceSettingsView />
</StackPanel>
</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="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
@ -45,17 +46,43 @@
<Reference Include="WindowsBase" />
</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="GetVolume\DeviceGetVolumeActionSettingsViewModel.cs" />
<Compile Include="GetVolume\DeviceGetVolumeActionSettingsView.xaml.cs">
<DependentUpon>DeviceGetVolumeActionSettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="MassiveKnobCoreAudioPlugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Settings\DeviceVolumeActionSettings.cs" />
<Compile Include="Settings\DeviceVolumeActionSettingsView.xaml.cs">
<DependentUpon>DeviceVolumeActionSettingsView.xaml</DependentUpon>
<Compile Include="GetVolume\DeviceGetVolumeActionSettings.cs" />
<Compile Include="SetVolume\DeviceSetVolumeActionSettings.cs" />
<Compile Include="SetVolume\DeviceSetVolumeActionSettingsView.xaml.cs">
<DependentUpon>DeviceSetVolumeActionSettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="Settings\BaseDeviceSettings.cs" />
<Compile Include="Settings\BaseDeviceSettingsViewModel.cs" />
<Compile Include="Settings\DeviceVolumeActionSettingsViewModel.cs" />
<Compile Include="Base\BaseDeviceSettings.cs" />
<Compile Include="Base\BaseDeviceSettingsViewModel.cs" />
<Compile Include="SetVolume\DeviceSetVolumeActionSettingsViewModel.cs" />
<Compile Include="Strings.Designer.cs">
<DependentUpon>Strings.resx</DependentUpon>
<AutoGen>True</AutoGen>
@ -72,21 +99,49 @@
<PackageReference Include="AudioSwitcher.AudioApi.CoreAudio">
<Version>4.0.0-alpha5</Version>
</PackageReference>
<PackageReference Include="System.Reactive">
<Version>5.0.0</Version>
</PackageReference>
<PackageReference Include="System.Threading.Tasks.Extensions">
<Version>4.5.4</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Strings.resx">
<Generator>ResXFileCodeGenerator</Generator>
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</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>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
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
{
@ -9,14 +12,17 @@ namespace MassiveKnob.Plugin.CoreAudio
public class MassiveKnobCoreAudioPlugin : IMassiveKnobActionPlugin
{
public Guid PluginId { get; } = new Guid("eaa5d3f8-8f9b-4a4b-8e29-827228d23e95");
public string Name { get; } = "Windows Core Audio";
public string Description { get; } = "Included with Massive Knob by default. Provides volume control per device and default device switching.";
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 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.Windows.Controls;
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 MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputAnalog;
public string Name { get; } = "Set volume";
public string Description { get; } = "Sets the volume for the selected device, regardless of the current default device.";
public string Name { get; } = Strings.SetVolumeName;
public string Description { get; } = Strings.SetVolumeDescription;
public IMassiveKnobActionInstance Create()
@ -23,14 +23,14 @@ namespace MassiveKnob.Plugin.CoreAudio.Actions
private class Instance : IMassiveKnobAnalogAction
{
private IMassiveKnobActionContext actionContext;
private DeviceVolumeActionSettings settings;
private DeviceSetVolumeActionSettings settings;
private IDevice playbackDevice;
public void Initialize(IMassiveKnobActionContext context)
{
actionContext = context;
settings = context.GetSettings<DeviceVolumeActionSettings>();
settings = context.GetSettings<DeviceSetVolumeActionSettings>();
ApplySettings();
}
@ -49,7 +49,7 @@ namespace MassiveKnob.Plugin.CoreAudio.Actions
public UserControl CreateSettingsControl()
{
var viewModel = new DeviceVolumeActionSettingsViewModel(settings);
var viewModel = new DeviceSetVolumeActionSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) =>
{
if (!viewModel.IsSettingsProperty(args.PropertyName))
@ -59,7 +59,7 @@ namespace MassiveKnob.Plugin.CoreAudio.Actions
ApplySettings();
};
return new DeviceVolumeActionSettingsView(viewModel);
return new DeviceSetVolumeActionSettingsView(viewModel);
}
@ -69,6 +69,9 @@ namespace MassiveKnob.Plugin.CoreAudio.Actions
return;
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.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Strings {
public class Strings {
private static global::System.Resources.ResourceManager resourceMan;
@ -36,7 +36,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
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.CoreAudio.Strings", typeof(Strings).Assembly);
@ -51,7 +51,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
@ -63,7 +63,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary>
/// Looks up a localized string similar to {0}.
/// </summary>
internal static string DeviceDisplayNameActive {
public static string DeviceDisplayNameActive {
get {
return ResourceManager.GetString("DeviceDisplayNameActive", resourceCulture);
}
@ -72,7 +72,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary>
/// Looks up a localized string similar to {0} (Disabled).
/// </summary>
internal static string DeviceDisplayNameDisabled {
public static string DeviceDisplayNameDisabled {
get {
return ResourceManager.GetString("DeviceDisplayNameDisabled", resourceCulture);
}
@ -81,7 +81,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary>
/// Looks up a localized string similar to {0} (Inactive).
/// </summary>
internal static string DeviceDisplayNameInactive {
public static string DeviceDisplayNameInactive {
get {
return ResourceManager.GetString("DeviceDisplayNameInactive", resourceCulture);
}
@ -90,7 +90,7 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary>
/// Looks up a localized string similar to {0} (Not present).
/// </summary>
internal static string DeviceDisplayNameNotPresent {
public static string DeviceDisplayNameNotPresent {
get {
return ResourceManager.GetString("DeviceDisplayNameNotPresent", resourceCulture);
}
@ -99,10 +99,154 @@ namespace MassiveKnob.Plugin.CoreAudio {
/// <summary>
/// Looks up a localized string similar to {0} (Unplugged).
/// </summary>
internal static string DeviceDisplayNameUnplugged {
public static string DeviceDisplayNameUnplugged {
get {
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">
<value>{0} (Unplugged)</value>
</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>

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>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MassiveKnob.Plugin.MockDevice</RootNamespace>
<AssemblyName>MassiveKnob.Plugin.MockDevice</AssemblyName>
<RootNamespace>MassiveKnob.Plugin.EmulatorDevice</RootNamespace>
<AssemblyName>MassiveKnob.Plugin.EmulatorDevice</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
@ -45,14 +45,18 @@
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="Devices\MockDevice.cs" />
<Compile Include="MassiveKnobMockDevicePlugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Settings\MockDeviceSettingsView.xaml.cs">
<DependentUpon>MockDeviceSettingsView.xaml</DependentUpon>
<Compile Include="Devices\EmulatorDevice.cs" />
<Compile Include="Devices\EmulatorDeviceWindow.xaml.cs">
<DependentUpon>EmulatorDeviceWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Settings\MockDeviceSettings.cs" />
<Compile Include="Settings\MockDeviceSettingsViewModel.cs" />
<Compile Include="Devices\EmulatorDeviceWindowViewModel.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>
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
@ -62,7 +66,11 @@
</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>
<Generator>MSBuild:Compile</Generator>
</Page>

View File

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
namespace MassiveKnob.Plugin.MockDevice
namespace MassiveKnob.Plugin.EmulatorDevice
{
[MassiveKnobPlugin]
public class MassiveKnobMockDevicePlugin : IMassiveKnobDevicePlugin
public class MassiveKnobEmulatorDevicePlugin : IMassiveKnobDevicePlugin
{
public Guid PluginId { get; } = new Guid("85f04232-d70f-494c-94a2-41452591ffb3");
public string Name { get; } = "Mock Device";
@ -14,7 +14,7 @@ namespace MassiveKnob.Plugin.MockDevice
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);
}
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')
{
// TODO support multiple I/O's
var knobCount = serialPort.ReadByte();
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.
/// </summary>
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.CoreAudio", "MassiveKnob.Plugin.CoreAudio\MassiveKnob.Plugin.CoreAudio.csproj", "{5BD5E2F2-9923-4F74-AC69-ACDA0B847937}"
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.SerialDevice", "MassiveKnob.Plugin.SerialDevice\MassiveKnob.Plugin.SerialDevice.csproj", "{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}"
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">
<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)
{
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)
return;
if (list[index].Context != context)
if (list[index]?.Context != context)
throw new InvalidOperationException("Caller must be the active action to retrieve the settings");
lock (settingsLock)
@ -273,7 +273,7 @@ namespace MassiveKnob.Model
return;
var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog);
if (mapping == null || analogInputIndex >= mapping.Count)
if (mapping == null || analogInputIndex >= mapping.Count || mapping[analogInputIndex] == null)
return;
if (mapping[analogInputIndex].ActionInfo.Instance is IMassiveKnobAnalogAction analogAction)
@ -286,8 +286,8 @@ namespace MassiveKnob.Model
if (context != activeDeviceContext)
return;
var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog);
if (mapping == null || digitalInputIndex >= mapping.Count)
var mapping = GetActionMappingList(MassiveKnobActionType.InputDigital);
if (mapping == null || digitalInputIndex >= mapping.Count || mapping[digitalInputIndex] == null)
return;
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)
{
switch (actionType)
@ -534,15 +566,15 @@ namespace MassiveKnob.Model
}
public void SetDigitalOutput(bool on)
{
throw new NotImplementedException();
}
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 TargetType="TextBlock" x:Key="SubHeader">
<Setter Property="Background" Value="LightSlateGray" />
<Setter Property="Foreground" Value="White" />
<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 TargetType="StackPanel" x:Key="Content">

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ using MassiveKnob.Plugin;
namespace MassiveKnob.ViewModel
{
// TODO better design-time version
public class SettingsViewModel : INotifyPropertyChanged
{
private readonly IMassiveKnobOrchestrator orchestrator;
@ -65,8 +66,10 @@ namespace MassiveKnob.ViewModel
{
specs = value;
OnPropertyChanged();
OnOtherPropertyChanged("AnalogInputVisibility");
OnOtherPropertyChanged("DigitalInputVisibility");
OnDependantPropertyChanged("AnalogInputVisibility");
OnDependantPropertyChanged("DigitalInputVisibility");
OnDependantPropertyChanged("AnalogOutputVisibility");
OnDependantPropertyChanged("DigitalOutputVisibility");
AnalogInputs = Enumerable
.Range(0, specs?.AnalogInputCount ?? 0)
@ -75,6 +78,14 @@ namespace MassiveKnob.ViewModel
DigitalInputs = Enumerable
.Range(0, specs?.DigitalInputCount ?? 0)
.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));
}
protected virtual void OnOtherPropertyChanged(string propertyName)
protected virtual void OnDependantPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}