diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.h b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.h index a900b5cca1..691226778d 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.h +++ b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.h @@ -31,7 +31,7 @@ template requires std::copy_constructible && std::default_initializable class SendableChooser : public SendableChooserBase { wpi::StringMap m_choices; - + std::function m_listener; template static U _unwrap_smart_ptr(const U& value); @@ -82,6 +82,14 @@ class SendableChooser : public SendableChooserBase { */ auto GetSelected() -> decltype(_unwrap_smart_ptr(m_choices[""])); + /** + * Bind a listener that's called when the selected value changes. + * Only one listener can be bound. Calling this function will replace the + * previous listener. + * @param listener The function to call that accepts the new value + */ + void OnChange(std::function); + void InitSendable(nt::NTSendableBuilder& builder) override; }; diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.inc b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.inc index c15f5aa445..5a18928e4b 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.inc +++ b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.inc @@ -48,6 +48,13 @@ auto SendableChooser::GetSelected() } } +template + requires std::copy_constructible && std::default_initializable +void SendableChooser::OnChange(std::function listener) { + std::scoped_lock lock(m_mutex); + m_listener = listener; +} + template requires std::copy_constructible && std::default_initializable void SendableChooser::InitSendable(nt::NTSendableBuilder& builder) { @@ -95,11 +102,23 @@ void SendableChooser::InitSendable(nt::NTSendableBuilder& builder) { nullptr); builder.AddStringProperty(kSelected, nullptr, [=, this](std::string_view val) { - std::scoped_lock lock(m_mutex); - m_haveSelected = true; - m_selected = val; - for (auto& pub : m_activePubs) { - pub.Set(val); + T choice{}; + std::function listener; + { + std::scoped_lock lock(m_mutex); + m_haveSelected = true; + m_selected = val; + for (auto& pub : m_activePubs) { + pub.Set(val); + } + if (m_previousVal != val && m_listener) { + choice = m_choices[val]; + listener = m_listener; + } + m_previousVal = val; + } + if (listener) { + listener(choice); } }); } diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooserBase.h b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooserBase.h index b5df73cc1a..46cbf0444b 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooserBase.h +++ b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooserBase.h @@ -45,6 +45,7 @@ class SendableChooserBase : public nt::NTSendable, wpi::SmallVector m_activePubs; wpi::mutex m_mutex; int m_instance; + std::string m_previousVal; static std::atomic_int s_instances; }; diff --git a/wpilibc/src/test/native/cpp/smartdashboard/SendableChooserTest.cpp b/wpilibc/src/test/native/cpp/smartdashboard/SendableChooserTest.cpp index c26fad6b3f..0d36018a38 100644 --- a/wpilibc/src/test/native/cpp/smartdashboard/SendableChooserTest.cpp +++ b/wpilibc/src/test/native/cpp/smartdashboard/SendableChooserTest.cpp @@ -56,5 +56,22 @@ TEST(SendableChooserTest, EXPECT_EQ(0, chooser.GetSelected()); } +TEST(SendableChooserTest, ChangeListener) { + frc::SendableChooser chooser; + + for (int i = 1; i <= 3; i++) { + chooser.AddOption(std::to_string(i), i); + } + int currentVal = 0; + chooser.OnChange([&](int val) { currentVal = val; }); + + frc::SmartDashboard::PutData("chooser", &chooser); + frc::SmartDashboard::UpdateValues(); + frc::SmartDashboard::PutString("chooser/selected", "3"); + frc::SmartDashboard::UpdateValues(); + + EXPECT_EQ(3, currentVal); +} + INSTANTIATE_TEST_SUITE_P(SendableChooserTests, SendableChooserTest, ::testing::Values(0, 1, 2, 3)); diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java index c967e17099..33d218f0da 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; /** * The {@link SendableChooser} class is a useful tool for presenting a selection of options to the @@ -48,6 +49,8 @@ public class SendableChooser implements NTSendable, AutoCloseable { private String m_defaultChoice = ""; private final int m_instance; + private String m_previousVal; + private Consumer m_listener; private static final AtomicInteger s_instances = new AtomicInteger(); /** Instantiates a {@link SendableChooser}. */ @@ -114,6 +117,19 @@ public class SendableChooser implements NTSendable, AutoCloseable { } } + /** + * Bind a listener that's called when the selected value changes. Only one listener can be bound. + * Calling this function will replace the previous listener. + * + * @param listener The function to call that accepts the new value + */ + public void onChange(Consumer listener) { + requireNonNullParam(listener, "listener", "onChange"); + m_mutex.lock(); + m_listener = listener; + m_mutex.unlock(); + } + private String m_selected; private final List m_activePubs = new ArrayList<>(); private final ReentrantLock m_mutex = new ReentrantLock(); @@ -151,15 +167,28 @@ public class SendableChooser implements NTSendable, AutoCloseable { SELECTED, null, val -> { + V choice; + Consumer listener; m_mutex.lock(); try { m_selected = val; + if (!m_selected.equals(m_previousVal) && m_listener != null) { + choice = m_map.get(val); + listener = m_listener; + } else { + choice = null; + listener = null; + } + m_previousVal = val; for (StringPublisher pub : m_activePubs) { pub.set(val); } } finally { m_mutex.unlock(); } + if (listener != null) { + listener.accept(choice); + } }); } } diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooserTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooserTest.java index c6bccca1d6..5c07191dc3 100644 --- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooserTest.java +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooserTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import edu.wpi.first.networktables.NetworkTableInstance; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -64,6 +65,23 @@ class SendableChooserTest { } } + @Test + void testChangeListener() { + try (var chooser = new SendableChooser()) { + for (int i = 1; i <= 3; i++) { + chooser.addOption(String.valueOf(i), i); + } + AtomicInteger currentVal = new AtomicInteger(); + chooser.onChange(val -> currentVal.set(val)); + + SmartDashboard.putData("chooser", chooser); + SmartDashboard.updateValues(); + SmartDashboard.putString("chooser/selected", "3"); + SmartDashboard.updateValues(); + assertEquals(3, currentVal.get()); + } + } + @AfterEach void tearDown() { m_inst.close();