diff --git a/Hardware/Massive Knob housing - 2 pots, 3 buttons, under desk, Arduino Pro Micro.f3d b/Hardware/Massive Knob housing - 2 pots, 3 buttons, under desk, Arduino Pro Micro.f3d new file mode 100644 index 0000000..23fa4ec Binary files /dev/null and b/Hardware/Massive Knob housing - 2 pots, 3 buttons, under desk, Arduino Pro Micro.f3d differ diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs index b2c921b..17c184f 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs @@ -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); } diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultAction.cs index 4026741..251b661 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultAction.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultAction.cs @@ -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); } diff --git a/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsViewModel.cs index c2d0b3e..55c2e20 100644 --- a/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsViewModel.cs +++ b/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsViewModel.cs @@ -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) { diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml.cs index 31f0dc4..7898e40 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml.cs @@ -1,4 +1,6 @@ -namespace MassiveKnob.Plugin.VoiceMeeter.Base +using System; + +namespace MassiveKnob.Plugin.VoiceMeeter.Base { /// /// Interaction logic for BaseVoiceMeeterSettingsView.xaml diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsViewModel.cs index d3bf795..18bca53 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsViewModel.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsViewModel.cs @@ -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 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 diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs index afbec34..ac76b15 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs @@ -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); }); } } } -} +} \ No newline at end of file diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml.cs index c62f2d9..10daa31 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml.cs @@ -1,14 +1,22 @@ -namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter +using System; + +namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter { /// /// Interaction logic for VoiceMeeterGetParameterActionSettingsView.xaml /// - public partial class VoiceMeeterGetParameterActionSettingsView + public partial class VoiceMeeterGetParameterActionSettingsView : IDisposable { public VoiceMeeterGetParameterActionSettingsView(VoiceMeeterGetParameterActionSettingsViewModel viewModel) { DataContext = viewModel; InitializeComponent(); } + + + public void Dispose() + { + (DataContext as VoiceMeeterGetParameterActionSettingsViewModel)?.Dispose(); + } } } \ No newline at end of file diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnob.Plugin.VoiceMeeter.csproj b/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnob.Plugin.VoiceMeeter.csproj index 260bd28..76aaf7d 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnob.Plugin.VoiceMeeter.csproj +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnob.Plugin.VoiceMeeter.csproj @@ -106,5 +106,10 @@ Designer + + + 5.0.0 + + \ No newline at end of file diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroAction.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroAction.cs index e1d6bf1..d786243 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroAction.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroAction.cs @@ -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); } } diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml.cs index 692c590..2020978 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml.cs @@ -1,14 +1,22 @@ -namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro +using System; + +namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro { /// /// Interaction logic for VoiceMeeterRunMacroActionSettingsView.xaml /// - 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(); + } } } diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsViewModel.cs index 8c67693..d8af847 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsViewModel.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsViewModel.cs @@ -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 + public class VoiceMeeterRunMacroActionSettingsViewModel : BaseVoiceMeeterSettingsViewModel, IDisposable { - // ReSharper disable UnusedMember.Global - used by WPF Bindingpriv + private readonly Subject throttledScriptChanged = new Subject(); + 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(); } } } diff --git a/Windows/MassiveKnob/Core/IMassiveKnobOrchestrator.cs b/Windows/MassiveKnob/Core/IMassiveKnobOrchestrator.cs index a13b2d0..45ef9c2 100644 --- a/Windows/MassiveKnob/Core/IMassiveKnobOrchestrator.cs +++ b/Windows/MassiveKnob/Core/IMassiveKnobOrchestrator.cs @@ -11,12 +11,24 @@ namespace MassiveKnob.Core MassiveKnobDeviceInfo SetActiveDevice(IMassiveKnobDevice device); + MassiveKnobDeviceStatus DeviceStatus { get; } + IObservable DeviceStatusSubject { get; } + + MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index); MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action); MassiveKnobSettings GetSettings(); void UpdateSettings(Action applyChanges); } + + + public enum MassiveKnobDeviceStatus + { + Disconnected, + Connecting, + Connected + } public class MassiveKnobDeviceInfo diff --git a/Windows/MassiveKnob/Core/MassiveKnobOrchestrator.cs b/Windows/MassiveKnob/Core/MassiveKnobOrchestrator.cs index 47e423f..5c5fac7 100644 --- a/Windows/MassiveKnob/Core/MassiveKnobOrchestrator.cs +++ b/Windows/MassiveKnob/Core/MassiveKnobOrchestrator.cs @@ -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 activeDeviceInfoSubject = new Subject(); + private readonly Subject deviceStatusSubject = new Subject(); private IMassiveKnobDeviceContext activeDeviceContext; private readonly List analogInputs = new List(); @@ -48,12 +49,15 @@ namespace MassiveKnob.Core public IObservable ActiveDeviceSubject => activeDeviceInfoSubject; + public MassiveKnobDeviceStatus DeviceStatus { get; private set; } = MassiveKnobDeviceStatus.Disconnected; + public IObservable 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(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() ?? new T(); + return settings.Device.Settings?.ToObject() ?? new T(); } } @@ -242,23 +249,40 @@ namespace MassiveKnob.Core protected void SetDeviceSettings(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(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); } diff --git a/Windows/MassiveKnob/Strings.Designer.cs b/Windows/MassiveKnob/Strings.Designer.cs index 34f5acc..60fd47e 100644 --- a/Windows/MassiveKnob/Strings.Designer.cs +++ b/Windows/MassiveKnob/Strings.Designer.cs @@ -69,6 +69,33 @@ namespace MassiveKnob { } } + /// + /// Looks up a localized string similar to Connected. + /// + public static string DeviceStatusConnected { + get { + return ResourceManager.GetString("DeviceStatusConnected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connecting.... + /// + public static string DeviceStatusConnecting { + get { + return ResourceManager.GetString("DeviceStatusConnecting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disconnected. + /// + public static string DeviceStatusDisconnected { + get { + return ResourceManager.GetString("DeviceStatusDisconnected", resourceCulture); + } + } + /// /// Looks up a localized string similar to Input #{0}. /// diff --git a/Windows/MassiveKnob/Strings.resx b/Windows/MassiveKnob/Strings.resx index bb908fd..4924581 100644 --- a/Windows/MassiveKnob/Strings.resx +++ b/Windows/MassiveKnob/Strings.resx @@ -120,6 +120,15 @@ Not configured + + Connected + + + Connecting... + + + Disconnected + Input #{0} diff --git a/Windows/MassiveKnob/View/Settings/DeviceView.xaml b/Windows/MassiveKnob/View/Settings/DeviceView.xaml index c540ea7..334a619 100644 --- a/Windows/MassiveKnob/View/Settings/DeviceView.xaml +++ b/Windows/MassiveKnob/View/Settings/DeviceView.xaml @@ -39,6 +39,11 @@ + + + + + diff --git a/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs b/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs index 0c4ff78..f4b4703 100644 --- a/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs +++ b/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs @@ -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 menuItemControls = new Dictionary @@ -47,6 +48,8 @@ namespace MassiveKnob.ViewModel private IEnumerable analogOutputs; private IEnumerable 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 Devices { get; } public IList 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 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 @@ -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; diff --git a/Windows/VoicemeeterRemote b/Windows/VoicemeeterRemote index 5d259cd..e0e17e5 160000 --- a/Windows/VoicemeeterRemote +++ b/Windows/VoicemeeterRemote @@ -1 +1 @@ -Subproject commit 5d259cdaee942029487e37a02e9a32ed9833d80c +Subproject commit e0e17e56feca7987a567a324132f785f1548a33f