1
0
mirror of synced 2024-11-21 15:43:49 +00:00

Fixed bug in caching output values

Fixed VoiceMeeter version selection
Added device status indicator
This commit is contained in:
Mark van Renswoude 2021-03-07 10:38:56 +01:00
parent 2525fae237
commit 69a93f68d5
19 changed files with 334 additions and 118 deletions

View File

@ -80,7 +80,7 @@ namespace MassiveKnob.Plugin.CoreAudio.GetDefault
CheckActive(); CheckActive();
// TODO default OSD // TODO (should have) OSD for changing default
//if (settings.OSD) //if (settings.OSD)
//OSDManager.Show(args.Device); //OSDManager.Show(args.Device);
} }

View File

@ -75,7 +75,7 @@ namespace MassiveKnob.Plugin.CoreAudio.SetDefault
await playbackDevice.SetAsDefaultCommunicationsAsync(); await playbackDevice.SetAsDefaultCommunicationsAsync();
// TODO OSD for default device // TODO (should have) OSD for changing default
//if (settings.OSD) //if (settings.OSD)
//OSDManager.Show(playbackDevice); //OSDManager.Show(playbackDevice);
} }

View File

@ -98,12 +98,6 @@ namespace MassiveKnob.Plugin.SerialDevice.Settings
} }
protected virtual void OnOtherPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnNext(DeviceNotificationEvent value) public void OnNext(DeviceNotificationEvent value)
{ {

View File

@ -1,4 +1,6 @@
namespace MassiveKnob.Plugin.VoiceMeeter.Base using System;
namespace MassiveKnob.Plugin.VoiceMeeter.Base
{ {
/// <summary> /// <summary>
/// Interaction logic for BaseVoiceMeeterSettingsView.xaml /// Interaction logic for BaseVoiceMeeterSettingsView.xaml

View File

@ -1,5 +1,7 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Voicemeeter; using Voicemeeter;
@ -16,10 +18,11 @@ namespace MassiveKnob.Plugin.VoiceMeeter.Base
public class BaseVoiceMeeterSettingsViewModel : INotifyPropertyChanged public class BaseVoiceMeeterSettingsViewModel : INotifyPropertyChanged, IDisposable
{ {
protected readonly BaseVoiceMeeterSettings Settings; protected readonly BaseVoiceMeeterSettings Settings;
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler Disposed;
// ReSharper disable UnusedMember.Global - used by WPF Binding // ReSharper disable UnusedMember.Global - used by WPF Binding
public IList<VoiceMeeterVersionViewModel> Versions { get; } public IList<VoiceMeeterVersionViewModel> Versions { get; }
@ -52,6 +55,14 @@ namespace MassiveKnob.Plugin.VoiceMeeter.Base
new VoiceMeeterVersionViewModel(RunVoicemeeterParam.VoicemeeterBanana, "VoiceMeeter Banana"), new VoiceMeeterVersionViewModel(RunVoicemeeterParam.VoicemeeterBanana, "VoiceMeeter Banana"),
new VoiceMeeterVersionViewModel(RunVoicemeeterParam.VoicemeeterPotato, "VoiceMeeter Potato") new VoiceMeeterVersionViewModel(RunVoicemeeterParam.VoicemeeterPotato, "VoiceMeeter Potato")
}; };
UpdateSelectedVersion();
}
private void UpdateSelectedVersion()
{
selectedVersion = Versions.SingleOrDefault(v => v.Version == Settings.Version) ?? Versions.First();
} }
@ -63,10 +74,28 @@ namespace MassiveKnob.Plugin.VoiceMeeter.Base
} }
public virtual void VoiceMeeterVersionChanged()
{
UpdateSelectedVersion();
OnDependantPropertyChanged(nameof(SelectedVersion));
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
protected virtual void OnDependantPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public virtual void Dispose()
{
Disposed?.Invoke(this, EventArgs.Empty);
}
} }
public class VoiceMeeterVersionViewModel public class VoiceMeeterVersionViewModel

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Collections.Generic;
using System.Runtime.Remoting.Channels;
using System.Windows.Controls; using System.Windows.Controls;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Voicemeeter; using Voicemeeter;
@ -12,18 +13,19 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputDigital; public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputDigital;
public string Name { get; } = Strings.GetParameterName; public string Name { get; } = Strings.GetParameterName;
public string Description { get; } = Strings.GetParameterDescription; public string Description { get; } = Strings.GetParameterDescription;
public IMassiveKnobActionInstance Create(ILogger logger) public IMassiveKnobActionInstance Create(ILogger logger)
{ {
return new Instance(); return new Instance();
} }
private class Instance : IMassiveKnobActionInstance, IVoiceMeeterAction private class Instance : IMassiveKnobActionInstance, IVoiceMeeterAction
{ {
private IMassiveKnobActionContext actionContext; private IMassiveKnobActionContext actionContext;
private VoiceMeeterGetParameterActionSettings settings; private VoiceMeeterGetParameterActionSettings settings;
private VoiceMeeterGetParameterActionSettingsViewModel viewModel;
private Parameters parameters; private Parameters parameters;
private IDisposable parameterChanged; private IDisposable parameterChanged;
@ -48,51 +50,54 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
private void ApplySettings() private void ApplySettings()
{ {
if (InstanceRegister.Version == RunVoicemeeterParam.None) if (InstanceRegister.Version == RunVoicemeeterParam.None || string.IsNullOrEmpty(settings.Parameter))
{
parameterChanged?.Dispose();
parameterChanged = null;
parameters?.Dispose();
parameters = null;
return; return;
}
if (parameters == null) if (parameters == null)
parameters = new Parameters(); parameters = new Parameters();
if (string.IsNullOrEmpty(settings.Parameter))
{
parameterChanged?.Dispose();
parameterChanged = null;
}
if (parameterChanged == null) if (parameterChanged == null)
parameterChanged = parameters.Subscribe(x => ParametersChanged()); parameterChanged = parameters.Subscribe(x => ParametersChanged());
// TODO directly update output depending on value ParametersChanged();
/*
if (playbackDevice != null)
actionContext.SetDigitalOutput(settings.Inverted ? !playbackDevice.IsMuted : playbackDevice.IsMuted);
*/
} }
public UserControl CreateSettingsControl() public UserControl CreateSettingsControl()
{ {
var viewModel = new VoiceMeeterGetParameterActionSettingsViewModel(settings); viewModel = new VoiceMeeterGetParameterActionSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) => viewModel.PropertyChanged += (sender, args) =>
{ {
if (!viewModel.IsSettingsProperty(args.PropertyName)) if (!viewModel.IsSettingsProperty(args.PropertyName))
return; return;
actionContext.SetSettings(settings); actionContext.SetSettings(settings);
ApplySettings(); ApplySettings();
}; };
viewModel.Disposed += (sender, args) =>
{
if (sender == viewModel)
viewModel = null;
};
return new VoiceMeeterGetParameterActionSettingsView(viewModel); return new VoiceMeeterGetParameterActionSettingsView(viewModel);
} }
public void VoiceMeeterVersionChanged() public void VoiceMeeterVersionChanged()
{ {
// TODO update viewModel viewModel?.VoiceMeeterVersionChanged();
// TODO reset parameterChanged subscription
actionContext.SetSettings(settings); actionContext.SetSettings(settings);
ApplySettings();
} }
@ -101,11 +106,8 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
if (InstanceRegister.Version == RunVoicemeeterParam.None || string.IsNullOrEmpty(settings.Parameter)) if (InstanceRegister.Version == RunVoicemeeterParam.None || string.IsNullOrEmpty(settings.Parameter))
return; return;
// TODO if another task is already running, wait / chain InstanceRegister.InitializeVoicemeeter().ContinueWith(t =>
// TODO only start task if not yet initialized
Task.Run(async () =>
{ {
await InstanceRegister.InitializeVoicemeeter();
bool on; bool on;
if (float.TryParse(settings.Value, out var settingsFloatValue)) if (float.TryParse(settings.Value, out var settingsFloatValue))
@ -130,10 +132,9 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
on = string.Equals(value, settings.Value, StringComparison.InvariantCultureIgnoreCase); on = string.Equals(value, settings.Value, StringComparison.InvariantCultureIgnoreCase);
} }
// TODO check specific parameter for changes, not just any parameter
actionContext.SetDigitalOutput(settings.Inverted ? !on : on); actionContext.SetDigitalOutput(settings.Inverted ? !on : on);
}); });
} }
} }
} }
} }

View File

@ -1,14 +1,22 @@
namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter using System;
namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
{ {
/// <summary> /// <summary>
/// Interaction logic for VoiceMeeterGetParameterActionSettingsView.xaml /// Interaction logic for VoiceMeeterGetParameterActionSettingsView.xaml
/// </summary> /// </summary>
public partial class VoiceMeeterGetParameterActionSettingsView public partial class VoiceMeeterGetParameterActionSettingsView : IDisposable
{ {
public VoiceMeeterGetParameterActionSettingsView(VoiceMeeterGetParameterActionSettingsViewModel viewModel) public VoiceMeeterGetParameterActionSettingsView(VoiceMeeterGetParameterActionSettingsViewModel viewModel)
{ {
DataContext = viewModel; DataContext = viewModel;
InitializeComponent(); InitializeComponent();
} }
public void Dispose()
{
(DataContext as VoiceMeeterGetParameterActionSettingsViewModel)?.Dispose();
}
} }
} }

View File

@ -106,5 +106,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="System.Reactive">
<Version>5.0.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Controls; using System.Windows.Controls;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -24,6 +25,7 @@ namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
{ {
private IMassiveKnobActionContext actionContext; private IMassiveKnobActionContext actionContext;
private VoiceMeeterRunMacroActionSettings settings; private VoiceMeeterRunMacroActionSettings settings;
private VoiceMeeterRunMacroActionSettingsViewModel viewModel;
public void Initialize(IMassiveKnobActionContext context) public void Initialize(IMassiveKnobActionContext context)
@ -43,7 +45,7 @@ namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
public UserControl CreateSettingsControl() public UserControl CreateSettingsControl()
{ {
var viewModel = new VoiceMeeterRunMacroActionSettingsViewModel(settings); viewModel = new VoiceMeeterRunMacroActionSettingsViewModel(settings);
viewModel.PropertyChanged += (sender, args) => viewModel.PropertyChanged += (sender, args) =>
{ {
if (!viewModel.IsSettingsProperty(args.PropertyName)) if (!viewModel.IsSettingsProperty(args.PropertyName))
@ -52,6 +54,12 @@ namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
actionContext.SetSettings(settings); actionContext.SetSettings(settings);
}; };
viewModel.Disposed += (sender, args) =>
{
if (sender == viewModel)
viewModel = null;
};
return new VoiceMeeterRunMacroActionSettingsView(viewModel); return new VoiceMeeterRunMacroActionSettingsView(viewModel);
} }
@ -71,8 +79,7 @@ namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
public void VoiceMeeterVersionChanged() public void VoiceMeeterVersionChanged()
{ {
// TODO update viewModel viewModel?.VoiceMeeterVersionChanged();
actionContext.SetSettings(settings); actionContext.SetSettings(settings);
} }
} }

View File

@ -1,14 +1,22 @@
namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro using System;
namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
{ {
/// <summary> /// <summary>
/// Interaction logic for VoiceMeeterRunMacroActionSettingsView.xaml /// Interaction logic for VoiceMeeterRunMacroActionSettingsView.xaml
/// </summary> /// </summary>
public partial class VoiceMeeterRunMacroActionSettingsView public partial class VoiceMeeterRunMacroActionSettingsView : IDisposable
{ {
// ReSharper disable once SuggestBaseTypeForParameter
public VoiceMeeterRunMacroActionSettingsView(VoiceMeeterRunMacroActionSettingsViewModel viewModel) public VoiceMeeterRunMacroActionSettingsView(VoiceMeeterRunMacroActionSettingsViewModel viewModel)
{ {
DataContext = viewModel; DataContext = viewModel;
InitializeComponent(); InitializeComponent();
} }
public void Dispose()
{
(DataContext as VoiceMeeterRunMacroActionSettingsViewModel)?.Dispose();
}
} }
} }

View File

@ -1,21 +1,26 @@
using MassiveKnob.Plugin.VoiceMeeter.Base; using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using MassiveKnob.Plugin.VoiceMeeter.Base;
namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
{ {
public class VoiceMeeterRunMacroActionSettingsViewModel : BaseVoiceMeeterSettingsViewModel<VoiceMeeterRunMacroActionSettings> public class VoiceMeeterRunMacroActionSettingsViewModel : BaseVoiceMeeterSettingsViewModel<VoiceMeeterRunMacroActionSettings>, IDisposable
{ {
// ReSharper disable UnusedMember.Global - used by WPF Bindingpriv private readonly Subject<bool> throttledScriptChanged = new Subject<bool>();
private readonly IDisposable scriptChangedSubscription;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public string Script public string Script
{ {
get => Settings.Script; get => Settings.Script;
set set
{ {
// TODO timer for change notification
if (value == Settings.Script) if (value == Settings.Script)
return; return;
Settings.Script = value; Settings.Script = value;
OnPropertyChanged(); throttledScriptChanged.OnNext(true);
} }
} }
// ReSharper restore UnusedMember.Global // ReSharper restore UnusedMember.Global
@ -24,6 +29,19 @@ namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
// ReSharper disable once SuggestBaseTypeForParameter - by design // ReSharper disable once SuggestBaseTypeForParameter - by design
public VoiceMeeterRunMacroActionSettingsViewModel(VoiceMeeterRunMacroActionSettings settings) : base(settings) public VoiceMeeterRunMacroActionSettingsViewModel(VoiceMeeterRunMacroActionSettings settings) : base(settings)
{ {
scriptChangedSubscription = throttledScriptChanged
.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(b =>
{
OnDependantPropertyChanged(nameof(Script));
});
}
public override void Dispose()
{
scriptChangedSubscription?.Dispose();
throttledScriptChanged?.Dispose();
} }
} }
} }

View File

@ -11,12 +11,24 @@ namespace MassiveKnob.Core
MassiveKnobDeviceInfo SetActiveDevice(IMassiveKnobDevice device); MassiveKnobDeviceInfo SetActiveDevice(IMassiveKnobDevice device);
MassiveKnobDeviceStatus DeviceStatus { get; }
IObservable<MassiveKnobDeviceStatus> DeviceStatusSubject { get; }
MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index); MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index);
MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action); MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action);
MassiveKnobSettings GetSettings(); MassiveKnobSettings GetSettings();
void UpdateSettings(Action<MassiveKnobSettings> applyChanges); void UpdateSettings(Action<MassiveKnobSettings> applyChanges);
} }
public enum MassiveKnobDeviceStatus
{
Disconnected,
Connecting,
Connected
}
public class MassiveKnobDeviceInfo public class MassiveKnobDeviceInfo

View File

@ -17,11 +17,12 @@ namespace MassiveKnob.Core
private readonly ILogger logger; private readonly ILogger logger;
private readonly object settingsLock = new object(); private readonly object settingsLock = new object();
private MassiveKnobSettings massiveKnobSettings; private readonly MassiveKnobSettings settings;
private readonly SerialQueue flushSettingsQueue = new SerialQueue(); private readonly SerialQueue flushSettingsQueue = new SerialQueue();
private MassiveKnobDeviceInfo activeDevice; private MassiveKnobDeviceInfo activeDevice;
private readonly Subject<MassiveKnobDeviceInfo> activeDeviceInfoSubject = new Subject<MassiveKnobDeviceInfo>(); private readonly Subject<MassiveKnobDeviceInfo> activeDeviceInfoSubject = new Subject<MassiveKnobDeviceInfo>();
private readonly Subject<MassiveKnobDeviceStatus> deviceStatusSubject = new Subject<MassiveKnobDeviceStatus>();
private IMassiveKnobDeviceContext activeDeviceContext; private IMassiveKnobDeviceContext activeDeviceContext;
private readonly List<ActionMapping> analogInputs = new List<ActionMapping>(); private readonly List<ActionMapping> analogInputs = new List<ActionMapping>();
@ -48,12 +49,15 @@ namespace MassiveKnob.Core
public IObservable<MassiveKnobDeviceInfo> ActiveDeviceSubject => activeDeviceInfoSubject; public IObservable<MassiveKnobDeviceInfo> ActiveDeviceSubject => activeDeviceInfoSubject;
public MassiveKnobDeviceStatus DeviceStatus { get; private set; } = MassiveKnobDeviceStatus.Disconnected;
public IObservable<MassiveKnobDeviceStatus> DeviceStatusSubject => deviceStatusSubject;
public MassiveKnobOrchestrator(IPluginManager pluginManager, ILogger logger, MassiveKnobSettings massiveKnobSettings)
public MassiveKnobOrchestrator(IPluginManager pluginManager, ILogger logger, MassiveKnobSettings settings)
{ {
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
this.logger = logger; this.logger = logger;
this.massiveKnobSettings = massiveKnobSettings; this.settings = settings;
} }
@ -87,11 +91,11 @@ namespace MassiveKnob.Core
{ {
lock (settingsLock) lock (settingsLock)
{ {
if (massiveKnobSettings.Device == null) if (settings.Device == null)
return; return;
var allDevices = pluginManager.GetDevicePlugins().SelectMany(dp => dp.Devices); var allDevices = pluginManager.GetDevicePlugins().SelectMany(dp => dp.Devices);
var device = allDevices.FirstOrDefault(d => d.DeviceId == massiveKnobSettings.Device.DeviceId); var device = allDevices.FirstOrDefault(d => d.DeviceId == settings.Device.DeviceId);
InternalSetActiveDevice(device, false); InternalSetActiveDevice(device, false);
} }
@ -105,7 +109,7 @@ namespace MassiveKnob.Core
return InternalSetActiveDevice(device, true); return InternalSetActiveDevice(device, true);
} }
public MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index) public MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index)
{ {
lock (settingsLock) lock (settingsLock)
@ -166,7 +170,7 @@ namespace MassiveKnob.Core
{ {
lock (settingsLock) lock (settingsLock)
{ {
return massiveKnobSettings.Clone(); return settings.Clone();
} }
} }
@ -175,7 +179,7 @@ namespace MassiveKnob.Core
{ {
lock (settingsLock) lock (settingsLock)
{ {
applyChanges(massiveKnobSettings); applyChanges(settings);
} }
FlushSettings(); FlushSettings();
@ -193,10 +197,10 @@ namespace MassiveKnob.Core
lock (settingsLock) lock (settingsLock)
{ {
if (device == null) if (device == null)
massiveKnobSettings.Device = null; settings.Device = null;
else else
{ {
massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings settings.Device = new MassiveKnobSettings.DeviceSettings
{ {
DeviceId = device.DeviceId, DeviceId = device.DeviceId,
Settings = null Settings = null
@ -208,7 +212,10 @@ namespace MassiveKnob.Core
} }
ActiveDevice?.Instance.Dispose(); ActiveDevice?.Instance.Dispose();
SetDeviceStatus(null, MassiveKnobDeviceStatus.Disconnected);
// TODO (must have) move initialization to separate Task, to prevent issues at startup
// TODO (must have) exception handling!
if (device != null) if (device != null)
{ {
var instance = device.Create(new SerilogLoggerProvider(logger.ForContext("Context", new { Device = device.DeviceId })).CreateLogger(null)); var instance = device.Create(new SerilogLoggerProvider(logger.ForContext("Context", new { Device = device.DeviceId })).CreateLogger(null));
@ -230,11 +237,11 @@ namespace MassiveKnob.Core
protected T GetDeviceSettings<T>(IMassiveKnobDeviceContext context) where T : class, new() protected T GetDeviceSettings<T>(IMassiveKnobDeviceContext context) where T : class, new()
{ {
if (context != activeDeviceContext) if (context != activeDeviceContext)
throw new InvalidOperationException("Caller must be the active device to retrieve the massiveKnobSettings"); throw new InvalidOperationException("Caller must be the active device to retrieve the settings");
lock (settingsLock) lock (settingsLock)
{ {
return massiveKnobSettings.Device.Settings?.ToObject<T>() ?? new T(); return settings.Device.Settings?.ToObject<T>() ?? new T();
} }
} }
@ -242,23 +249,40 @@ namespace MassiveKnob.Core
protected void SetDeviceSettings<T>(IMassiveKnobDeviceContext context, IMassiveKnobDevice device, T deviceSettings) where T : class, new() protected void SetDeviceSettings<T>(IMassiveKnobDeviceContext context, IMassiveKnobDevice device, T deviceSettings) where T : class, new()
{ {
if (context != activeDeviceContext) if (context != activeDeviceContext)
throw new InvalidOperationException("Caller must be the active device to update the massiveKnobSettings"); throw new InvalidOperationException("Caller must be the active device to update the settings");
lock (settingsLock) lock (settingsLock)
{ {
if (massiveKnobSettings.Device == null) if (settings.Device == null)
massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings settings.Device = new MassiveKnobSettings.DeviceSettings
{ {
DeviceId = device.DeviceId DeviceId = device.DeviceId
}; };
massiveKnobSettings.Device.Settings = JObject.FromObject(deviceSettings); settings.Device.Settings = JObject.FromObject(deviceSettings);
} }
FlushSettings(); FlushSettings();
} }
protected void SetDeviceStatus(IMassiveKnobDeviceContext context, MassiveKnobDeviceStatus status)
{
if (context != null && context != activeDeviceContext)
return;
lock (settingsLock)
{
if (status == DeviceStatus)
return;
DeviceStatus = status;
}
deviceStatusSubject.OnNext(status);
}
protected T GetActionSettings<T>(IMassiveKnobActionContext context, IMassiveKnobAction action, int index) where T : class, new() protected T GetActionSettings<T>(IMassiveKnobActionContext context, IMassiveKnobAction action, int index) where T : class, new()
{ {
lock (settingsLock) lock (settingsLock)
@ -268,7 +292,7 @@ namespace MassiveKnob.Core
return new T(); return new T();
if (list[index]?.Context != context) if (list[index]?.Context != context)
throw new InvalidOperationException("Caller must be the active action to retrieve the massiveKnobSettings"); throw new InvalidOperationException("Caller must be the active action to retrieve the settings");
var settingsList = GetActionSettingsList(action.ActionType); var settingsList = GetActionSettingsList(action.ActionType);
if (index >= settingsList.Count) if (index >= settingsList.Count)
@ -288,7 +312,7 @@ namespace MassiveKnob.Core
return; return;
if (list[index]?.Context != context) if (list[index]?.Context != context)
throw new InvalidOperationException("Caller must be the active action to retrieve the massiveKnobSettings"); throw new InvalidOperationException("Caller must be the active action to retrieve the settings");
var settingsList = GetActionSettingsList(action.ActionType); var settingsList = GetActionSettingsList(action.ActionType);
@ -317,11 +341,6 @@ namespace MassiveKnob.Core
lock (settingsLock) lock (settingsLock)
{ {
if (analogOutputValues.TryGetValue(analogInputIndex, out var currentValue) && currentValue == value)
return;
analogOutputValues[analogInputIndex] = value;
var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog); var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog);
if (mapping == null || analogInputIndex >= mapping.Count || mapping[analogInputIndex] == null) if (mapping == null || analogInputIndex >= mapping.Count || mapping[analogInputIndex] == null)
return; return;
@ -343,12 +362,6 @@ namespace MassiveKnob.Core
lock (settingsLock) lock (settingsLock)
{ {
if (digitalOutputValues.TryGetValue(digitalInputIndex, out var currentValue) && currentValue == on)
return;
digitalOutputValues[digitalInputIndex] = on;
var mapping = GetActionMappingList(MassiveKnobActionType.InputDigital); var mapping = GetActionMappingList(MassiveKnobActionType.InputDigital);
if (mapping == null || digitalInputIndex >= mapping.Count || mapping[digitalInputIndex] == null) if (mapping == null || digitalInputIndex >= mapping.Count || mapping[digitalInputIndex] == null)
return; return;
@ -362,13 +375,19 @@ namespace MassiveKnob.Core
public void SetAnalogOutput(IMassiveKnobActionContext context, int index, byte value) public void SetAnalogOutput(IMassiveKnobActionContext context, int index, byte value)
{ {
if (activeDevice == null)
return;
IMassiveKnobDeviceInstance deviceInstance; IMassiveKnobDeviceInstance deviceInstance;
lock (settingsLock) lock (settingsLock)
{ {
if (analogOutputValues.TryGetValue(index, out var currentValue) && currentValue == value)
return;
analogOutputValues[index] = value;
if (activeDevice == null)
return;
var list = GetActionMappingList(MassiveKnobActionType.OutputAnalog); var list = GetActionMappingList(MassiveKnobActionType.OutputAnalog);
if (index >= list.Count) if (index >= list.Count)
return; return;
@ -385,13 +404,19 @@ namespace MassiveKnob.Core
public void SetDigitalOutput(IMassiveKnobActionContext context, int index, bool on) public void SetDigitalOutput(IMassiveKnobActionContext context, int index, bool on)
{ {
if (activeDevice == null)
return;
IMassiveKnobDeviceInstance deviceInstance; IMassiveKnobDeviceInstance deviceInstance;
lock (settingsLock) lock (settingsLock)
{ {
if (digitalOutputValues.TryGetValue(index, out var currentValue) && currentValue == on)
return;
digitalOutputValues[index] = on;
if (activeDevice == null)
return;
var list = GetActionMappingList(MassiveKnobActionType.OutputDigital); var list = GetActionMappingList(MassiveKnobActionType.OutputDigital);
if (index >= list.Count) if (index >= list.Count)
return; return;
@ -433,16 +458,16 @@ namespace MassiveKnob.Core
switch (actionType) switch (actionType)
{ {
case MassiveKnobActionType.InputAnalog: case MassiveKnobActionType.InputAnalog:
return massiveKnobSettings.AnalogInput; return settings.AnalogInput;
case MassiveKnobActionType.InputDigital: case MassiveKnobActionType.InputDigital:
return massiveKnobSettings.DigitalInput; return settings.DigitalInput;
case MassiveKnobActionType.OutputAnalog: case MassiveKnobActionType.OutputAnalog:
return massiveKnobSettings.AnalogOutput; return settings.AnalogOutput;
case MassiveKnobActionType.OutputDigital: case MassiveKnobActionType.OutputDigital:
return massiveKnobSettings.DigitalOutput; return settings.DigitalOutput;
default: default:
throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null); throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null);
@ -455,7 +480,7 @@ namespace MassiveKnob.Core
lock (settingsLock) lock (settingsLock)
{ {
massiveKnobSettingsSnapshot = massiveKnobSettings.Clone(); massiveKnobSettingsSnapshot = settings.Clone();
} }
flushSettingsQueue.Enqueue(async () => flushSettingsQueue.Enqueue(async () =>
@ -482,10 +507,10 @@ namespace MassiveKnob.Core
lock (settingsLock) lock (settingsLock)
{ {
UpdateMapping(analogInputs, specs.AnalogInputCount, massiveKnobSettings.AnalogInput, DelayedInitialize); UpdateMapping(analogInputs, specs.AnalogInputCount, settings.AnalogInput, DelayedInitialize);
UpdateMapping(digitalInputs, specs.DigitalInputCount, massiveKnobSettings.DigitalInput, DelayedInitialize); UpdateMapping(digitalInputs, specs.DigitalInputCount, settings.DigitalInput, DelayedInitialize);
UpdateMapping(analogOutputs, specs.AnalogOutputCount, massiveKnobSettings.AnalogOutput, DelayedInitialize); UpdateMapping(analogOutputs, specs.AnalogOutputCount, settings.AnalogOutput, DelayedInitialize);
UpdateMapping(digitalOutputs, specs.DigitalOutputCount, massiveKnobSettings.DigitalOutput, DelayedInitialize); UpdateMapping(digitalOutputs, specs.DigitalOutputCount, settings.DigitalOutput, DelayedInitialize);
} }
foreach (var delayedInitializeAction in delayedInitializeActions) foreach (var delayedInitializeAction in delayedInitializeActions)
@ -603,21 +628,20 @@ namespace MassiveKnob.Core
public void Connecting() public void Connecting()
{ {
// TODO (should have) update status ? owner.SetDeviceStatus(this, MassiveKnobDeviceStatus.Connecting);
} }
public void Connected(DeviceSpecs specs) public void Connected(DeviceSpecs specs)
{ {
// TODO (should have) update status ? owner.SetDeviceStatus(this, MassiveKnobDeviceStatus.Connected);
owner.UpdateActiveDeviceSpecs(this, specs); owner.UpdateActiveDeviceSpecs(this, specs);
} }
public void Disconnected() public void Disconnected()
{ {
// TODO (should have) update status ? owner.SetDeviceStatus(this, MassiveKnobDeviceStatus.Disconnected);
} }

View File

@ -69,6 +69,33 @@ namespace MassiveKnob {
} }
} }
/// <summary>
/// Looks up a localized string similar to Connected.
/// </summary>
public static string DeviceStatusConnected {
get {
return ResourceManager.GetString("DeviceStatusConnected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connecting....
/// </summary>
public static string DeviceStatusConnecting {
get {
return ResourceManager.GetString("DeviceStatusConnecting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Disconnected.
/// </summary>
public static string DeviceStatusDisconnected {
get {
return ResourceManager.GetString("DeviceStatusDisconnected", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Input #{0}. /// Looks up a localized string similar to Input #{0}.
/// </summary> /// </summary>

View File

@ -120,6 +120,15 @@
<data name="ActionNotConfigured" xml:space="preserve"> <data name="ActionNotConfigured" xml:space="preserve">
<value>Not configured</value> <value>Not configured</value>
</data> </data>
<data name="DeviceStatusConnected" xml:space="preserve">
<value>Connected</value>
</data>
<data name="DeviceStatusConnecting" xml:space="preserve">
<value>Connecting...</value>
</data>
<data name="DeviceStatusDisconnected" xml:space="preserve">
<value>Disconnected</value>
</data>
<data name="InputHeader" xml:space="preserve"> <data name="InputHeader" xml:space="preserve">
<value>Input #{0}</value> <value>Input #{0}</value>
</data> </data>

View File

@ -39,6 +39,11 @@
<ContentControl Focusable="False" Content="{Binding SettingsControl}" Style="{StaticResource SettingsControl}" /> <ContentControl Focusable="False" Content="{Binding SettingsControl}" Style="{StaticResource SettingsControl}" />
<StackPanel Margin="0,24,0,0" Orientation="Horizontal">
<Ellipse Margin="0,0,4,0" Fill="{Binding ConnectionStatusColor}" Height="8" Width="8" />
<TextBlock Text="{Binding ConnectionStatusText}" />
</StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@ -8,6 +8,7 @@ using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media;
using MassiveKnob.Core; using MassiveKnob.Core;
using MassiveKnob.Plugin; using MassiveKnob.Plugin;
using MassiveKnob.Settings; using MassiveKnob.Settings;
@ -18,7 +19,7 @@ using Serilog.Events;
namespace MassiveKnob.ViewModel namespace MassiveKnob.ViewModel
{ {
// TODO (code quality) split ViewModel for individual views, create viewmodel using container // TODO (code quality) split ViewModel for individual views, create viewmodel using container
// TODO (must have) show device status // TODO (nice to have) installed plugins list
public class SettingsViewModel : IDisposable, INotifyPropertyChanged public class SettingsViewModel : IDisposable, INotifyPropertyChanged
{ {
private readonly Dictionary<SettingsMenuItem, Type> menuItemControls = new Dictionary<SettingsMenuItem, Type> private readonly Dictionary<SettingsMenuItem, Type> menuItemControls = new Dictionary<SettingsMenuItem, Type>
@ -47,6 +48,8 @@ namespace MassiveKnob.ViewModel
private IEnumerable<InputOutputViewModel> analogOutputs; private IEnumerable<InputOutputViewModel> analogOutputs;
private IEnumerable<InputOutputViewModel> digitalOutputs; private IEnumerable<InputOutputViewModel> digitalOutputs;
private IDisposable activeDeviceSubscription;
private IDisposable deviceStatusSubscription;
// ReSharper disable UnusedMember.Global - used by WPF Binding // ReSharper disable UnusedMember.Global - used by WPF Binding
public SettingsMenuItem SelectedMenuItem public SettingsMenuItem SelectedMenuItem
@ -77,18 +80,18 @@ namespace MassiveKnob.ViewModel
{ {
if (value == selectedView) if (value == selectedView)
return; return;
selectedView = value; selectedView = value;
OnPropertyChanged(); OnPropertyChanged();
} }
} }
public IList<DeviceViewModel> Devices { get; } public IList<DeviceViewModel> Devices { get; }
public IList<ActionViewModel> Actions { get; } public IList<ActionViewModel> Actions { get; }
public DeviceViewModel SelectedDevice public DeviceViewModel SelectedDevice
{ {
get => selectedDevice; get => selectedDevice;
@ -141,23 +144,27 @@ namespace MassiveKnob.ViewModel
AnalogInputs = Enumerable AnalogInputs = Enumerable
.Range(0, specs?.AnalogInputCount ?? 0) .Range(0, specs?.AnalogInputCount ?? 0)
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.InputAnalog, i)); .Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.InputAnalog, i))
.ToList();
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))
.ToList();
AnalogOutputs = Enumerable AnalogOutputs = Enumerable
.Range(0, specs?.AnalogOutputCount ?? 0) .Range(0, specs?.AnalogOutputCount ?? 0)
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.OutputAnalog, i)); .Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.OutputAnalog, i))
.ToList();
DigitalOutputs = Enumerable DigitalOutputs = Enumerable
.Range(0, specs?.DigitalOutputCount ?? 0) .Range(0, specs?.DigitalOutputCount ?? 0)
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.OutputDigital, i)); .Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.OutputDigital, i))
.ToList();
} }
} }
public Visibility AnalogInputVisibility => specs.HasValue && specs.Value.AnalogInputCount > 0 public Visibility AnalogInputVisibility => specs.HasValue && specs.Value.AnalogInputCount > 0
? Visibility.Visible ? Visibility.Visible
: Visibility.Collapsed; : Visibility.Collapsed;
@ -213,11 +220,12 @@ namespace MassiveKnob.ViewModel
OnPropertyChanged(); OnPropertyChanged();
} }
} }
public IList<LoggingLevelViewModel> LoggingLevels { get; } public IList<LoggingLevelViewModel> LoggingLevels { get; }
private LoggingLevelViewModel selectedLoggingLevel; private LoggingLevelViewModel selectedLoggingLevel;
public LoggingLevelViewModel SelectedLoggingLevel public LoggingLevelViewModel SelectedLoggingLevel
{ {
get => selectedLoggingLevel; get => selectedLoggingLevel;
@ -225,7 +233,7 @@ namespace MassiveKnob.ViewModel
{ {
if (value == selectedLoggingLevel) if (value == selectedLoggingLevel)
return; return;
selectedLoggingLevel = value; selectedLoggingLevel = value;
OnPropertyChanged(); OnPropertyChanged();
@ -235,6 +243,7 @@ namespace MassiveKnob.ViewModel
private bool loggingEnabled; private bool loggingEnabled;
public bool LoggingEnabled public bool LoggingEnabled
{ {
get => loggingEnabled; get => loggingEnabled;
@ -245,14 +254,16 @@ namespace MassiveKnob.ViewModel
loggingEnabled = value; loggingEnabled = value;
OnPropertyChanged(); OnPropertyChanged();
ApplyLoggingSettings(); ApplyLoggingSettings();
} }
} }
// TODO (code quality) do not hardcode path here // TODO (code quality) do not hardcode path here
public string LoggingOutputPath { get; } = string.Format(Strings.LoggingOutputPath, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Logs")); public string LoggingOutputPath { get; } = string.Format(Strings.LoggingOutputPath,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob",
@"Logs"));
private bool runAtStartup; private bool runAtStartup;
@ -270,6 +281,55 @@ namespace MassiveKnob.ViewModel
ApplyRunAtStartup(); ApplyRunAtStartup();
} }
} }
public string ConnectionStatusText
{
get
{
if (orchestrator == null)
return "Design-time";
switch (orchestrator.DeviceStatus)
{
case MassiveKnobDeviceStatus.Disconnected:
return Strings.DeviceStatusDisconnected;
case MassiveKnobDeviceStatus.Connecting:
return Strings.DeviceStatusConnecting;
case MassiveKnobDeviceStatus.Connected:
return Strings.DeviceStatusConnected;
default:
return null;
}
}
}
public Brush ConnectionStatusColor
{
get
{
if (orchestrator == null)
return Brushes.Fuchsia;
switch (orchestrator.DeviceStatus)
{
case MassiveKnobDeviceStatus.Disconnected:
return Brushes.DarkRed;
case MassiveKnobDeviceStatus.Connecting:
return Brushes.Orange;
case MassiveKnobDeviceStatus.Connected:
return Brushes.ForestGreen;
default:
return null;
}
}
}
// ReSharper restore UnusedMember.Global // ReSharper restore UnusedMember.Global
@ -288,11 +348,17 @@ namespace MassiveKnob.ViewModel
SelectedMenuItem = activeMenuItem; SelectedMenuItem = activeMenuItem;
orchestrator.ActiveDeviceSubject.Subscribe(info => { Specs = info.Specs; }); activeDeviceSubscription = orchestrator.ActiveDeviceSubject.Subscribe(info => { Specs = info.Specs; });
deviceStatusSubscription = orchestrator.DeviceStatusSubject.Subscribe(status =>
{
OnDependantPropertyChanged(nameof(ConnectionStatusColor));
OnDependantPropertyChanged(nameof(ConnectionStatusText));
});
Devices = pluginManager.GetDevicePlugins() Devices = pluginManager.GetDevicePlugins()
.SelectMany(dp => dp.Devices.Select(d => new DeviceViewModel(dp, d))) .SelectMany(dp => dp.Devices.Select(d => new DeviceViewModel(dp, d)))
.OrderBy(d => d.Name.ToLower())
.ToList(); .ToList();
var allActions = new List<ActionViewModel> var allActions = new List<ActionViewModel>
@ -302,7 +368,8 @@ namespace MassiveKnob.ViewModel
allActions.AddRange( allActions.AddRange(
pluginManager.GetActionPlugins() pluginManager.GetActionPlugins()
.SelectMany(ap => ap.Actions.Select(a => new ActionViewModel(ap, a)))); .SelectMany(ap => ap.Actions.Select(a => new ActionViewModel(ap, a)))
.OrderBy(a => a.Name.ToLower()));
Actions = allActions; Actions = allActions;

@ -1 +1 @@
Subproject commit 5d259cdaee942029487e37a02e9a32ed9833d80c Subproject commit e0e17e56feca7987a567a324132f785f1548a33f