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();
// TODO default OSD
// TODO (should have) OSD for changing default
//if (settings.OSD)
//OSDManager.Show(args.Device);
}

View File

@ -75,7 +75,7 @@ namespace MassiveKnob.Plugin.CoreAudio.SetDefault
await playbackDevice.SetAsDefaultCommunicationsAsync();
// TODO OSD for default device
// TODO (should have) OSD for changing default
//if (settings.OSD)
//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)
{

View File

@ -1,4 +1,6 @@
namespace MassiveKnob.Plugin.VoiceMeeter.Base
using System;
namespace MassiveKnob.Plugin.VoiceMeeter.Base
{
/// <summary>
/// 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.Linq;
using System.Runtime.CompilerServices;
using Voicemeeter;
@ -16,10 +18,11 @@ namespace MassiveKnob.Plugin.VoiceMeeter.Base
public class BaseVoiceMeeterSettingsViewModel : INotifyPropertyChanged
public class BaseVoiceMeeterSettingsViewModel : INotifyPropertyChanged, IDisposable
{
protected readonly BaseVoiceMeeterSettings Settings;
public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler Disposed;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public IList<VoiceMeeterVersionViewModel> Versions { get; }
@ -52,6 +55,14 @@ namespace MassiveKnob.Plugin.VoiceMeeter.Base
new VoiceMeeterVersionViewModel(RunVoicemeeterParam.VoicemeeterBanana, "VoiceMeeter Banana"),
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)
{
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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,22 @@
namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
using System;
namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
{
/// <summary>
/// Interaction logic for VoiceMeeterRunMacroActionSettingsView.xaml
/// </summary>
public partial class VoiceMeeterRunMacroActionSettingsView
public partial class VoiceMeeterRunMacroActionSettingsView : IDisposable
{
// ReSharper disable once SuggestBaseTypeForParameter
public VoiceMeeterRunMacroActionSettingsView(VoiceMeeterRunMacroActionSettingsViewModel viewModel)
{
DataContext = viewModel;
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
{
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
{
get => Settings.Script;
set
{
// TODO timer for change notification
if (value == Settings.Script)
return;
Settings.Script = value;
OnPropertyChanged();
throttledScriptChanged.OnNext(true);
}
}
// ReSharper restore UnusedMember.Global
@ -24,6 +29,19 @@ namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro
// ReSharper disable once SuggestBaseTypeForParameter - by design
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);
MassiveKnobDeviceStatus DeviceStatus { get; }
IObservable<MassiveKnobDeviceStatus> DeviceStatusSubject { get; }
MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index);
MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action);
MassiveKnobSettings GetSettings();
void UpdateSettings(Action<MassiveKnobSettings> applyChanges);
}
public enum MassiveKnobDeviceStatus
{
Disconnected,
Connecting,
Connected
}
public class MassiveKnobDeviceInfo

View File

@ -17,11 +17,12 @@ namespace MassiveKnob.Core
private readonly ILogger logger;
private readonly object settingsLock = new object();
private MassiveKnobSettings massiveKnobSettings;
private readonly MassiveKnobSettings settings;
private readonly SerialQueue flushSettingsQueue = new SerialQueue();
private MassiveKnobDeviceInfo activeDevice;
private readonly Subject<MassiveKnobDeviceInfo> activeDeviceInfoSubject = new Subject<MassiveKnobDeviceInfo>();
private readonly Subject<MassiveKnobDeviceStatus> deviceStatusSubject = new Subject<MassiveKnobDeviceStatus>();
private IMassiveKnobDeviceContext activeDeviceContext;
private readonly List<ActionMapping> analogInputs = new List<ActionMapping>();
@ -48,12 +49,15 @@ namespace MassiveKnob.Core
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.logger = logger;
this.massiveKnobSettings = massiveKnobSettings;
this.settings = settings;
}
@ -87,11 +91,11 @@ namespace MassiveKnob.Core
{
lock (settingsLock)
{
if (massiveKnobSettings.Device == null)
if (settings.Device == null)
return;
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);
}
@ -105,7 +109,7 @@ namespace MassiveKnob.Core
return InternalSetActiveDevice(device, true);
}
public MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index)
{
lock (settingsLock)
@ -166,7 +170,7 @@ namespace MassiveKnob.Core
{
lock (settingsLock)
{
return massiveKnobSettings.Clone();
return settings.Clone();
}
}
@ -175,7 +179,7 @@ namespace MassiveKnob.Core
{
lock (settingsLock)
{
applyChanges(massiveKnobSettings);
applyChanges(settings);
}
FlushSettings();
@ -193,10 +197,10 @@ namespace MassiveKnob.Core
lock (settingsLock)
{
if (device == null)
massiveKnobSettings.Device = null;
settings.Device = null;
else
{
massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings
settings.Device = new MassiveKnobSettings.DeviceSettings
{
DeviceId = device.DeviceId,
Settings = null
@ -208,7 +212,10 @@ namespace MassiveKnob.Core
}
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)
{
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()
{
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)
{
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()
{
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)
{
if (massiveKnobSettings.Device == null)
massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings
if (settings.Device == null)
settings.Device = new MassiveKnobSettings.DeviceSettings
{
DeviceId = device.DeviceId
};
massiveKnobSettings.Device.Settings = JObject.FromObject(deviceSettings);
settings.Device.Settings = JObject.FromObject(deviceSettings);
}
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()
{
lock (settingsLock)
@ -268,7 +292,7 @@ namespace MassiveKnob.Core
return new T();
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);
if (index >= settingsList.Count)
@ -288,7 +312,7 @@ namespace MassiveKnob.Core
return;
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);
@ -317,11 +341,6 @@ namespace MassiveKnob.Core
lock (settingsLock)
{
if (analogOutputValues.TryGetValue(analogInputIndex, out var currentValue) && currentValue == value)
return;
analogOutputValues[analogInputIndex] = value;
var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog);
if (mapping == null || analogInputIndex >= mapping.Count || mapping[analogInputIndex] == null)
return;
@ -343,12 +362,6 @@ namespace MassiveKnob.Core
lock (settingsLock)
{
if (digitalOutputValues.TryGetValue(digitalInputIndex, out var currentValue) && currentValue == on)
return;
digitalOutputValues[digitalInputIndex] = on;
var mapping = GetActionMappingList(MassiveKnobActionType.InputDigital);
if (mapping == null || digitalInputIndex >= mapping.Count || mapping[digitalInputIndex] == null)
return;
@ -362,13 +375,19 @@ namespace MassiveKnob.Core
public void SetAnalogOutput(IMassiveKnobActionContext context, int index, byte value)
{
if (activeDevice == null)
return;
IMassiveKnobDeviceInstance deviceInstance;
lock (settingsLock)
{
if (analogOutputValues.TryGetValue(index, out var currentValue) && currentValue == value)
return;
analogOutputValues[index] = value;
if (activeDevice == null)
return;
var list = GetActionMappingList(MassiveKnobActionType.OutputAnalog);
if (index >= list.Count)
return;
@ -385,13 +404,19 @@ namespace MassiveKnob.Core
public void SetDigitalOutput(IMassiveKnobActionContext context, int index, bool on)
{
if (activeDevice == null)
return;
IMassiveKnobDeviceInstance deviceInstance;
lock (settingsLock)
{
if (digitalOutputValues.TryGetValue(index, out var currentValue) && currentValue == on)
return;
digitalOutputValues[index] = on;
if (activeDevice == null)
return;
var list = GetActionMappingList(MassiveKnobActionType.OutputDigital);
if (index >= list.Count)
return;
@ -433,16 +458,16 @@ namespace MassiveKnob.Core
switch (actionType)
{
case MassiveKnobActionType.InputAnalog:
return massiveKnobSettings.AnalogInput;
return settings.AnalogInput;
case MassiveKnobActionType.InputDigital:
return massiveKnobSettings.DigitalInput;
return settings.DigitalInput;
case MassiveKnobActionType.OutputAnalog:
return massiveKnobSettings.AnalogOutput;
return settings.AnalogOutput;
case MassiveKnobActionType.OutputDigital:
return massiveKnobSettings.DigitalOutput;
return settings.DigitalOutput;
default:
throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null);
@ -455,7 +480,7 @@ namespace MassiveKnob.Core
lock (settingsLock)
{
massiveKnobSettingsSnapshot = massiveKnobSettings.Clone();
massiveKnobSettingsSnapshot = settings.Clone();
}
flushSettingsQueue.Enqueue(async () =>
@ -482,10 +507,10 @@ namespace MassiveKnob.Core
lock (settingsLock)
{
UpdateMapping(analogInputs, specs.AnalogInputCount, massiveKnobSettings.AnalogInput, DelayedInitialize);
UpdateMapping(digitalInputs, specs.DigitalInputCount, massiveKnobSettings.DigitalInput, DelayedInitialize);
UpdateMapping(analogOutputs, specs.AnalogOutputCount, massiveKnobSettings.AnalogOutput, DelayedInitialize);
UpdateMapping(digitalOutputs, specs.DigitalOutputCount, massiveKnobSettings.DigitalOutput, DelayedInitialize);
UpdateMapping(analogInputs, specs.AnalogInputCount, settings.AnalogInput, DelayedInitialize);
UpdateMapping(digitalInputs, specs.DigitalInputCount, settings.DigitalInput, DelayedInitialize);
UpdateMapping(analogOutputs, specs.AnalogOutputCount, settings.AnalogOutput, DelayedInitialize);
UpdateMapping(digitalOutputs, specs.DigitalOutputCount, settings.DigitalOutput, DelayedInitialize);
}
foreach (var delayedInitializeAction in delayedInitializeActions)
@ -603,21 +628,20 @@ namespace MassiveKnob.Core
public void Connecting()
{
// TODO (should have) update status ?
owner.SetDeviceStatus(this, MassiveKnobDeviceStatus.Connecting);
}
public void Connected(DeviceSpecs specs)
{
// TODO (should have) update status ?
owner.SetDeviceStatus(this, MassiveKnobDeviceStatus.Connected);
owner.UpdateActiveDeviceSpecs(this, specs);
}
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>
/// Looks up a localized string similar to Input #{0}.
/// </summary>

View File

@ -120,6 +120,15 @@
<data name="ActionNotConfigured" xml:space="preserve">
<value>Not configured</value>
</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">
<value>Input #{0}</value>
</data>

View File

@ -39,6 +39,11 @@
<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>
</UserControl>

View File

@ -8,6 +8,7 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using MassiveKnob.Core;
using MassiveKnob.Plugin;
using MassiveKnob.Settings;
@ -18,7 +19,7 @@ using Serilog.Events;
namespace MassiveKnob.ViewModel
{
// 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
{
private readonly Dictionary<SettingsMenuItem, Type> menuItemControls = new Dictionary<SettingsMenuItem, Type>
@ -47,6 +48,8 @@ namespace MassiveKnob.ViewModel
private IEnumerable<InputOutputViewModel> analogOutputs;
private IEnumerable<InputOutputViewModel> digitalOutputs;
private IDisposable activeDeviceSubscription;
private IDisposable deviceStatusSubscription;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public SettingsMenuItem SelectedMenuItem
@ -77,18 +80,18 @@ namespace MassiveKnob.ViewModel
{
if (value == selectedView)
return;
selectedView = value;
OnPropertyChanged();
}
}
public IList<DeviceViewModel> Devices { get; }
public IList<ActionViewModel> Actions { get; }
public DeviceViewModel SelectedDevice
{
get => selectedDevice;
@ -141,23 +144,27 @@ namespace MassiveKnob.ViewModel
AnalogInputs = Enumerable
.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
.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
.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
.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
? Visibility.Visible
: Visibility.Collapsed;
@ -213,11 +220,12 @@ namespace MassiveKnob.ViewModel
OnPropertyChanged();
}
}
public IList<LoggingLevelViewModel> LoggingLevels { get; }
private LoggingLevelViewModel selectedLoggingLevel;
public LoggingLevelViewModel SelectedLoggingLevel
{
get => selectedLoggingLevel;
@ -225,7 +233,7 @@ namespace MassiveKnob.ViewModel
{
if (value == selectedLoggingLevel)
return;
selectedLoggingLevel = value;
OnPropertyChanged();
@ -235,6 +243,7 @@ namespace MassiveKnob.ViewModel
private bool loggingEnabled;
public bool LoggingEnabled
{
get => loggingEnabled;
@ -245,14 +254,16 @@ namespace MassiveKnob.ViewModel
loggingEnabled = value;
OnPropertyChanged();
ApplyLoggingSettings();
}
}
// TODO (code quality) do not hardcode path here
public string LoggingOutputPath { get; } = string.Format(Strings.LoggingOutputPath, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Logs"));
public string LoggingOutputPath { get; } = string.Format(Strings.LoggingOutputPath,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob",
@"Logs"));
private bool runAtStartup;
@ -270,6 +281,55 @@ namespace MassiveKnob.ViewModel
ApplyRunAtStartup();
}
}
public string ConnectionStatusText
{
get
{
if (orchestrator == null)
return "Design-time";
switch (orchestrator.DeviceStatus)
{
case MassiveKnobDeviceStatus.Disconnected:
return Strings.DeviceStatusDisconnected;
case MassiveKnobDeviceStatus.Connecting:
return Strings.DeviceStatusConnecting;
case MassiveKnobDeviceStatus.Connected:
return Strings.DeviceStatusConnected;
default:
return null;
}
}
}
public Brush ConnectionStatusColor
{
get
{
if (orchestrator == null)
return Brushes.Fuchsia;
switch (orchestrator.DeviceStatus)
{
case MassiveKnobDeviceStatus.Disconnected:
return Brushes.DarkRed;
case MassiveKnobDeviceStatus.Connecting:
return Brushes.Orange;
case MassiveKnobDeviceStatus.Connected:
return Brushes.ForestGreen;
default:
return null;
}
}
}
// ReSharper restore UnusedMember.Global
@ -288,11 +348,17 @@ namespace MassiveKnob.ViewModel
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()
.SelectMany(dp => dp.Devices.Select(d => new DeviceViewModel(dp, d)))
.OrderBy(d => d.Name.ToLower())
.ToList();
var allActions = new List<ActionViewModel>
@ -302,7 +368,8 @@ namespace MassiveKnob.ViewModel
allActions.AddRange(
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;

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