From 913f740113893b2044002ef68615b4c066f1c0de Mon Sep 17 00:00:00 2001 From: Glenn Maynard Date: Mon, 23 Apr 2018 19:51:15 -0500 Subject: [PATCH] Allow configuring two pads together (the existing behavior), or selecting a single pad from a dropdown to configure them separately. --- smx-config/CurrentSMXDevice.cs | 12 --- smx-config/Helpers.cs | 59 +++++++++++++ smx-config/MainWindow.xaml | 24 +++--- smx-config/MainWindow.xaml.cs | 149 ++++++++++++++++++++++++++++++--- smx-config/SMX.cs | 59 +++++++++++++ smx-config/Widgets.cs | 50 ++++++----- 6 files changed, 292 insertions(+), 61 deletions(-) diff --git a/smx-config/CurrentSMXDevice.cs b/smx-config/CurrentSMXDevice.cs index 744e8d7..52f21a0 100644 --- a/smx-config/CurrentSMXDevice.cs +++ b/smx-config/CurrentSMXDevice.cs @@ -31,10 +31,6 @@ namespace smx_config // Data for each of two controllers: public LoadFromConfigDelegateArgsPerController[] controller; - // For convenience, this is the index in controller of the first connected controller. - // If no controllers are connected, this is 0. - public int FirstController; - // The control that changed the configuration (passed to FireConfigurationChanged). public object source; }; @@ -134,7 +130,6 @@ namespace smx_config public LoadFromConfigDelegateArgs GetState() { LoadFromConfigDelegateArgs args = new LoadFromConfigDelegateArgs(); - args.FirstController = -1; args.controller = new LoadFromConfigDelegateArgsPerController[2]; for(int pad = 0; pad < 2; ++pad) @@ -151,15 +146,8 @@ namespace smx_config SMX.SMX.GetConfig(pad, out controller.config); SMX.SMX.GetTestData(pad, out controller.test_data); args.controller[pad] = controller; - - // If this is the first connected controller, set FirstController. - if(controller.info.connected && args.FirstController == -1) - args.FirstController = pad; } - if(args.FirstController == -1) - args.FirstController = 0; - return args; } diff --git a/smx-config/Helpers.cs b/smx-config/Helpers.cs index 9cdf7fc..2e40b98 100644 --- a/smx-config/Helpers.cs +++ b/smx-config/Helpers.cs @@ -7,6 +7,65 @@ using SMXJSON; namespace smx_config { + // Track whether we're configuring one pad or both at once. + static class ActivePad + { + public enum SelectedPad { + P1, + P2, + Both, + }; + + // The actual pad selection. This defaults to both, and doesn't change if + // only one pad is selected. We don't actually show "both" in the dropdown + // unless two pads are connected, but the underlying setting remains. + public static SelectedPad selectedPad = SelectedPad.Both; + + // A shortcut for when a LoadFromConfigDelegateArgs isn't available: + public static IEnumerable> ActivePads() + { + return ActivePads(CurrentSMXDevice.singleton.GetState()); + } + + // Yield each connected pad which is currently active for configuration. + public static IEnumerable> ActivePads(LoadFromConfigDelegateArgs args) + { + bool Pad1Connected = args.controller[0].info.connected; + bool Pad2Connected = args.controller[1].info.connected; + + // If both pads are connected and a single pad is selected, ignore the deselected pad. + if(Pad1Connected && Pad2Connected) + { + if(selectedPad == SelectedPad.P1) + Pad2Connected = false; + if(selectedPad == SelectedPad.P2) + Pad1Connected = false; + } + + if(Pad1Connected) + yield return Tuple.Create(0, args.controller[0].config); + if(Pad2Connected) + yield return Tuple.Create(1, args.controller[1].config); + } + + // We know the selected pads are synced if there are two active, and when refreshing a + // UI we just want one of them to set the UI to. For convenience, return the first one. + public static SMX.SMXConfig GetFirstActivePadConfig(LoadFromConfigDelegateArgs args) + { + foreach(Tuple activePad in ActivePads(args)) + return activePad.Item2; + + // There aren't any pads connected. Just return a dummy config, since the UI + // isn't visible. + return new SMX.SMXConfig(); + } + + public static SMX.SMXConfig GetFirstActivePadConfig() + { + return GetFirstActivePadConfig(CurrentSMXDevice.singleton.GetState()); + } + } + static class Helpers { // Return true if we're in debug mode. diff --git a/smx-config/MainWindow.xaml b/smx-config/MainWindow.xaml index 3883f5f..7ff9bf4 100644 --- a/smx-config/MainWindow.xaml +++ b/smx-config/MainWindow.xaml @@ -632,16 +632,7 @@ Use if the platform is too sensitive. - - - - - - - + diff --git a/smx-config/MainWindow.xaml.cs b/smx-config/MainWindow.xaml.cs index c8a137f..494eda5 100644 --- a/smx-config/MainWindow.xaml.cs +++ b/smx-config/MainWindow.xaml.cs @@ -1,5 +1,8 @@ using System; +using System.Linq; +using System.Collections.Generic; using System.Windows; +using System.Windows.Controls; namespace smx_config { @@ -44,6 +47,7 @@ namespace smx_config if(!SMX.SMX.GetConfig(SelectedPad, out copyFromConfig)) return; + // Don't use ActivePad.ActivePads here, since the lights UI handles multiple pads on its own. for(int pad = 0; pad < 2; ++pad) { SMX.SMXConfig config; @@ -70,28 +74,148 @@ namespace smx_config Main.Visibility = EitherControllerConnected? Visibility.Visible:Visibility.Hidden; Searching.Visibility = EitherControllerConnected? Visibility.Hidden:Visibility.Visible; ConnectedPads.Visibility = EitherControllerConnected? Visibility.Visible:Visibility.Hidden; - P1Connected.IsEnabled = args.controller[0].info.connected; - P2Connected.IsEnabled = args.controller[1].info.connected; PanelColorP1.Visibility = args.controller[0].info.connected? Visibility.Visible:Visibility.Collapsed; PanelColorP2.Visibility = args.controller[1].info.connected? Visibility.Visible:Visibility.Collapsed; + + RefreshConnectedPadList(args); + + // If a second controller has connected and we're on Both, see if we need to prompt + // to sync configs. We only actually need to do this if a controller just connected. + if(args.ConfigurationChanged) + CheckConfiguringBothPads(args); + } + + private void ConnectedPadList_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + ComboBoxItem selection = ConnectedPadList.SelectedItem as ComboBoxItem; + ActivePad.SelectedPad newSelection; + if(selection == ConnectedPadList_P1) + newSelection = ActivePad.SelectedPad.P1; + else if(selection == ConnectedPadList_P2) + newSelection = ActivePad.SelectedPad.P2; + else + newSelection = ActivePad.SelectedPad.Both; + if(ActivePad.selectedPad == newSelection) + return; + + ActivePad.selectedPad = newSelection; + + // Before firing and updating UI, run CheckConfiguringBothPads to see if we should + // sync the config and/or change the selection again. + CheckConfiguringBothPads(CurrentSMXDevice.singleton.GetState()); + + CurrentSMXDevice.singleton.FireConfigurationChanged(null); + } + + // If the user selects "Both", or connects a second pad while on "Both", both pads need + // to have the same configuration, since we're configuring them together. Check if the + // configuration is out of sync, and ask the user before syncing them up so we don't + // clobber P2's configuration if this wasn't intended. + // + // If the user cancels, change the pad selection to P1 so we don't clobber P2. + private void CheckConfiguringBothPads(LoadFromConfigDelegateArgs args) + { + // Check if we're actually in "Both" mode with two controllers connected. If not, + // we don't have to do anything. + bool Pad1Connected = args.controller[0].info.connected; + bool Pad2Connected = args.controller[1].info.connected; + if(ActivePad.selectedPad != ActivePad.SelectedPad.Both || !Pad1Connected || !Pad2Connected) + return; + + // If the two pads have the same configuration, there's nothing to do. + SMX.SMXConfig config1 = args.controller[0].config; + SMX.SMXConfig config2 = args.controller[1].config; + if(ConfigurationsSynced(config1, config2)) + return; + + string messageBoxText = "The two pads have different settings. Do you want to " + + "match P2 settings to P1 and configure both pads together? (This won't affect panel colors.)"; + MessageBoxResult result = MessageBox.Show(messageBoxText, "StepManiaX", MessageBoxButton.YesNo, MessageBoxImage.None); + if(result == MessageBoxResult.Yes) + { + SyncP2FromP1(config1, config2); + return; + } + else + { + // Switch to P1. + ActivePad.selectedPad = ActivePad.SelectedPad.P1; + RefreshConnectedPadList(CurrentSMXDevice.singleton.GetState()); + } + + } + + // Return true if the two pads have the same configuration, so we can configure them together + // without clobbering separate configurations. + bool ConfigurationsSynced(SMX.SMXConfig config1, SMX.SMXConfig config2) + { + if(!Enumerable.SequenceEqual(config1.GetLowThresholds(), config2.GetLowThresholds())) + return false; + if(!Enumerable.SequenceEqual(config1.GetHighThresholds(), config2.GetHighThresholds())) + return false; + if(!Enumerable.SequenceEqual(config1.enabledSensors, config2.enabledSensors)) + return false; + return true; + } + + // Copy the P2 pad configuration to P1. + // + // This only copies settings that we actually configure, and it doesn't copy pad + // colors, which is separate from pad selection. + void SyncP2FromP1(SMX.SMXConfig config1, SMX.SMXConfig config2) + { + // Copy P1's configuration to P2. + Array.Copy(config1.enabledSensors, config2.enabledSensors, config1.enabledSensors.Count()); + config2.SetLowThresholds(config1.GetLowThresholds()); + config2.SetHighThresholds(config1.GetHighThresholds()); + SMX.SMX.SetConfig(1, config2); + CurrentSMXDevice.singleton.FireConfigurationChanged(null); + } + + // Refresh which items are visible in the connected pads list, and which item is displayed as selected. + private void RefreshConnectedPadList(LoadFromConfigDelegateArgs args) + { + bool TwoControllersConnected = args.controller[0].info.connected && args.controller[1].info.connected; + + // Only show the dropdown if two controllers are connected. + ConnectedPadList.Visibility = TwoControllersConnected? Visibility.Visible:Visibility.Collapsed; + + // Only show the P1/P2 text if only one controller is connected, since it takes the place + // of the dropdown. + P1Connected.Visibility = (!TwoControllersConnected && args.controller[0].info.connected)? Visibility.Visible:Visibility.Collapsed; + P2Connected.Visibility = (!TwoControllersConnected && args.controller[1].info.connected)? Visibility.Visible:Visibility.Collapsed; + + if(!TwoControllersConnected) + return; + + // Set the current selection. + ActivePad.SelectedPad selectedPad = ActivePad.selectedPad; + switch(ActivePad.selectedPad) + { + case ActivePad.SelectedPad.P1: ConnectedPadList.SelectedItem = ConnectedPadList_P1; break; + case ActivePad.SelectedPad.P2: ConnectedPadList.SelectedItem = ConnectedPadList_P2; break; + case ActivePad.SelectedPad.Both: ConnectedPadList.SelectedItem = ConnectedPadList_Both; break; + } } + private void FactoryReset_Click(object sender, RoutedEventArgs e) { - for(int pad = 0; pad < 2; ++pad) + foreach(Tuple activePad in ActivePad.ActivePads()) + { + int pad = activePad.Item1; SMX.SMX.FactoryReset(pad); + } CurrentSMXDevice.singleton.FireConfigurationChanged(null); } private void ExportSettings(object sender, RoutedEventArgs e) { // Save the current thresholds on the first available pad as a preset. - for(int pad = 0; pad < 2; ++pad) + foreach(Tuple activePad in ActivePad.ActivePads()) { - SMX.SMXConfig config; - if(!SMX.SMX.GetConfig(pad, out config)) - continue; - + int pad = activePad.Item1; + SMX.SMXConfig config = activePad.Item2; string json = SMXHelpers.ExportSettingsToJSON(config); Microsoft.Win32.SaveFileDialog dialog = new Microsoft.Win32.SaveFileDialog(); @@ -120,12 +244,11 @@ namespace smx_config string json = Helpers.ReadFile(dialog.FileName); - // Apply settings from the file to all connected pads. - for(int pad = 0; pad < 2; ++pad) + // Apply settings from the file to all active pads. + foreach(Tuple activePad in ActivePad.ActivePads()) { - SMX.SMXConfig config; - if(!SMX.SMX.GetConfig(pad, out config)) - continue; + int pad = activePad.Item1; + SMX.SMXConfig config = activePad.Item2; SMXHelpers.ImportSettingsFromJSON(json, ref config); SMX.SMX.SetConfig(pad, config); diff --git a/smx-config/SMX.cs b/smx-config/SMX.cs index 88bbcac..c319119 100644 --- a/smx-config/SMX.cs +++ b/smx-config/SMX.cs @@ -93,6 +93,65 @@ namespace SMX if(panels[7]) enabledSensors[3] |= 0x0F; if(panels[8]) enabledSensors[4] |= 0xF0; } + + // The layout of this structure (and the underlying C struct) matches the firmware configuration + // data. This is a bit inconvenient for the panel thresholds which aren't contiguous, so these + // helpers just convert them to and from arrays. + public Byte[] GetLowThresholds() + { + return new Byte[] { + panelThreshold0Low, + panelThreshold1Low, + panelThreshold2Low, + panelThreshold3Low, + panelThreshold4Low, + panelThreshold5Low, + panelThreshold6Low, + panelThreshold7Low, + panelThreshold8Low, + }; + } + + public Byte[] GetHighThresholds() + { + return new Byte[] { + panelThreshold0High, + panelThreshold1High, + panelThreshold2High, + panelThreshold3High, + panelThreshold4High, + panelThreshold5High, + panelThreshold6High, + panelThreshold7High, + panelThreshold8High, + }; + } + + public void SetLowThresholds(Byte[] values) + { + panelThreshold0Low = values[0]; + panelThreshold1Low = values[1]; + panelThreshold2Low = values[2]; + panelThreshold3Low = values[3]; + panelThreshold4Low = values[4]; + panelThreshold5Low = values[5]; + panelThreshold6Low = values[6]; + panelThreshold7Low = values[7]; + panelThreshold8Low = values[8]; + } + + public void SetHighThresholds(Byte[] values) + { + panelThreshold0High = values[0]; + panelThreshold1High = values[1]; + panelThreshold2High = values[2]; + panelThreshold3High = values[3]; + panelThreshold4High = values[4]; + panelThreshold5High = values[5]; + panelThreshold6High = values[6]; + panelThreshold7High = values[7]; + panelThreshold8High = values[8]; + } }; public struct SMXSensorTestModeData diff --git a/smx-config/Widgets.cs b/smx-config/Widgets.cs index fa79a78..08e1b39 100644 --- a/smx-config/Widgets.cs +++ b/smx-config/Widgets.cs @@ -35,7 +35,7 @@ namespace smx_config base.OnApplyTemplate(); onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { - LoadUIFromConfig(args.controller[args.FirstController].config); + LoadUIFromConfig(ActivePad.GetFirstActivePadConfig(args)); }); } @@ -65,12 +65,10 @@ namespace smx_config // Stop forcing advanced mode on, and sync the thresholds so we exit advanced mode. ForcedOn = false; - for(int pad = 0; pad < 2; ++pad) + foreach(Tuple activePad in ActivePad.ActivePads()) { - SMX.SMXConfig config; - if(!SMX.SMX.GetConfig(pad, out config)) - continue; - + int pad = activePad.Item1; + SMX.SMXConfig config = activePad.Item2; ConfigPresets.SyncUnifiedThresholds(ref config); SMX.SMX.SetConfig(pad, config); } @@ -83,8 +81,7 @@ namespace smx_config } // Refresh the UI. - LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState(); - LoadUIFromConfig(args.controller[args.FirstController].config); + LoadUIFromConfig(ActivePad.GetFirstActivePadConfig()); } } @@ -137,7 +134,7 @@ namespace smx_config slider.ValueChanged += delegate(DoubleSlider slider) { SaveToConfig(); }; onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { - LoadUIFromConfig(args.controller[args.FirstController].config); + LoadUIFromConfig(ActivePad.GetFirstActivePadConfig(args)); }); } @@ -193,11 +190,10 @@ namespace smx_config return; // Apply the change and save it to the devices. - for(int pad = 0; pad < 2; ++pad) + foreach(Tuple activePad in ActivePad.ActivePads()) { - SMX.SMXConfig config; - if(!SMX.SMX.GetConfig(pad, out config)) - continue; + int pad = activePad.Item1; + SMX.SMXConfig config = activePad.Item2; SetValueToConfig(ref config); SMX.SMX.SetConfig(pad, config); @@ -233,8 +229,7 @@ namespace smx_config void RefreshVisibility() { - LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState(); - SMX.SMXConfig config = args.controller[args.FirstController].config; + SMX.SMXConfig config = ActivePad.GetFirstActivePadConfig(); this.Visibility = ShouldBeDisplayed(config)? Visibility.Visible:Visibility.Collapsed; } @@ -299,18 +294,22 @@ namespace smx_config button.Click += delegate(object sender, RoutedEventArgs e) { Select(); }; onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { - string CurrentPreset = ConfigPresets.GetPreset(args.controller[args.FirstController].config); - Selected = CurrentPreset == Type; + foreach(Tuple activePad in ActivePad.ActivePads()) + { + SMX.SMXConfig config = activePad.Item2; + string CurrentPreset = ConfigPresets.GetPreset(config); + Selected = CurrentPreset == Type; + break; + } }); } private void Select() { - for(int pad = 0; pad < 2; ++pad) + foreach(Tuple activePad in ActivePad.ActivePads()) { - SMX.SMXConfig config; - if(!SMX.SMX.GetConfig(pad, out config)) - continue; + int pad = activePad.Item1; + SMX.SMXConfig config = activePad.Item2; ConfigPresets.SetPreset(Type, ref config); Console.WriteLine("PresetButton::Select (" + Type + "): " + @@ -724,7 +723,7 @@ namespace smx_config button.Click += EnabledPanelButtonClicked; onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) { - LoadUIFromConfig(args.controller[args.FirstController].config); + LoadUIFromConfig(ActivePad.GetFirstActivePadConfig(args)); }); } @@ -768,11 +767,10 @@ namespace smx_config Console.WriteLine("Clicked " + button); // Set the enabled sensor mask on both pads to the state of the UI. - for(int pad = 0; pad < 2; ++pad) + foreach(Tuple activePad in ActivePad.ActivePads()) { - SMX.SMXConfig config; - if(!SMX.SMX.GetConfig(pad, out config)) - continue; + int pad = activePad.Item1; + SMX.SMXConfig config = activePad.Item2; // This could be done algorithmically, but this is clearer. int[] PanelButtonToSensorIndex = {