001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.wpilibj2.command.button;
006
007import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
008
009import edu.wpi.first.math.filter.Debouncer;
010import edu.wpi.first.wpilibj.event.BooleanEvent;
011import edu.wpi.first.wpilibj.event.EventLoop;
012import edu.wpi.first.wpilibj2.command.Command;
013import edu.wpi.first.wpilibj2.command.CommandScheduler;
014import edu.wpi.first.wpilibj2.command.InstantCommand;
015import edu.wpi.first.wpilibj2.command.Subsystem;
016import java.util.function.BooleanSupplier;
017
018/**
019 * This class provides an easy way to link commands to conditions.
020 *
021 * <p>It is very easy to link a button to a command. For instance, you could link the trigger button
022 * of a joystick to a "score" command.
023 *
024 * <p>Triggers can easily be composed for advanced functionality using the {@link
025 * #and(BooleanSupplier)}, {@link #or(BooleanSupplier)}, {@link #negate()} operators.
026 *
027 * <p>This class is provided by the NewCommands VendorDep
028 */
029public class Trigger implements BooleanSupplier {
030  private final BooleanSupplier m_condition;
031  private final EventLoop m_loop;
032
033  /**
034   * Creates a new trigger based on the given condition.
035   *
036   * @param loop The loop instance that polls this trigger.
037   * @param condition the condition represented by this trigger
038   */
039  public Trigger(EventLoop loop, BooleanSupplier condition) {
040    m_loop = requireNonNullParam(loop, "loop", "Trigger");
041    m_condition = requireNonNullParam(condition, "condition", "Trigger");
042  }
043
044  /**
045   * Creates a new trigger based on the given condition.
046   *
047   * <p>Polled by the default scheduler button loop.
048   *
049   * @param condition the condition represented by this trigger
050   */
051  public Trigger(BooleanSupplier condition) {
052    this(CommandScheduler.getInstance().getDefaultButtonLoop(), condition);
053  }
054
055  /** Creates a new trigger that is always `false`. */
056  @Deprecated
057  public Trigger() {
058    this(() -> false);
059  }
060
061  /**
062   * Starts the given command whenever the condition changes from `false` to `true`.
063   *
064   * @param command the command to start
065   * @return this trigger, so calls can be chained
066   */
067  public Trigger onTrue(Command command) {
068    requireNonNullParam(command, "command", "onRising");
069    m_loop.bind(
070        new Runnable() {
071          private boolean m_pressedLast = m_condition.getAsBoolean();
072
073          @Override
074          public void run() {
075            boolean pressed = m_condition.getAsBoolean();
076
077            if (!m_pressedLast && pressed) {
078              command.schedule();
079            }
080
081            m_pressedLast = pressed;
082          }
083        });
084    return this;
085  }
086
087  /**
088   * Starts the given command whenever the condition changes from `true` to `false`.
089   *
090   * @param command the command to start
091   * @return this trigger, so calls can be chained
092   */
093  public Trigger onFalse(Command command) {
094    requireNonNullParam(command, "command", "onFalling");
095    m_loop.bind(
096        new Runnable() {
097          private boolean m_pressedLast = m_condition.getAsBoolean();
098
099          @Override
100          public void run() {
101            boolean pressed = m_condition.getAsBoolean();
102
103            if (m_pressedLast && !pressed) {
104              command.schedule();
105            }
106
107            m_pressedLast = pressed;
108          }
109        });
110    return this;
111  }
112
113  /**
114   * Starts the given command when the condition changes to `true` and cancels it when the condition
115   * changes to `false`.
116   *
117   * <p>Doesn't re-start the command if it ends while the condition is still `true`. If the command
118   * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}.
119   *
120   * @param command the command to start
121   * @return this trigger, so calls can be chained
122   */
123  public Trigger whileTrue(Command command) {
124    requireNonNullParam(command, "command", "whileHigh");
125    m_loop.bind(
126        new Runnable() {
127          private boolean m_pressedLast = m_condition.getAsBoolean();
128
129          @Override
130          public void run() {
131            boolean pressed = m_condition.getAsBoolean();
132
133            if (!m_pressedLast && pressed) {
134              command.schedule();
135            } else if (m_pressedLast && !pressed) {
136              command.cancel();
137            }
138
139            m_pressedLast = pressed;
140          }
141        });
142    return this;
143  }
144
145  /**
146   * Starts the given command when the condition changes to `false` and cancels it when the
147   * condition changes to `true`.
148   *
149   * <p>Doesn't re-start the command if it ends while the condition is still `false`. If the command
150   * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}.
151   *
152   * @param command the command to start
153   * @return this trigger, so calls can be chained
154   */
155  public Trigger whileFalse(Command command) {
156    requireNonNullParam(command, "command", "whileLow");
157    m_loop.bind(
158        new Runnable() {
159          private boolean m_pressedLast = m_condition.getAsBoolean();
160
161          @Override
162          public void run() {
163            boolean pressed = m_condition.getAsBoolean();
164
165            if (m_pressedLast && !pressed) {
166              command.schedule();
167            } else if (!m_pressedLast && pressed) {
168              command.cancel();
169            }
170
171            m_pressedLast = pressed;
172          }
173        });
174    return this;
175  }
176
177  /**
178   * Toggles a command when the condition changes from `false` to `true`.
179   *
180   * @param command the command to toggle
181   * @return this trigger, so calls can be chained
182   */
183  public Trigger toggleOnTrue(Command command) {
184    requireNonNullParam(command, "command", "toggleOnRising");
185    m_loop.bind(
186        new Runnable() {
187          private boolean m_pressedLast = m_condition.getAsBoolean();
188
189          @Override
190          public void run() {
191            boolean pressed = m_condition.getAsBoolean();
192
193            if (!m_pressedLast && pressed) {
194              if (command.isScheduled()) {
195                command.cancel();
196              } else {
197                command.schedule();
198              }
199            }
200
201            m_pressedLast = pressed;
202          }
203        });
204    return this;
205  }
206
207  /**
208   * Toggles a command when the condition changes from `true` to the low state.
209   *
210   * @param command the command to toggle
211   * @return this trigger, so calls can be chained
212   */
213  public Trigger toggleOnFalse(Command command) {
214    requireNonNullParam(command, "command", "toggleOnFalling");
215    m_loop.bind(
216        new Runnable() {
217          private boolean m_pressedLast = m_condition.getAsBoolean();
218
219          @Override
220          public void run() {
221            boolean pressed = m_condition.getAsBoolean();
222
223            if (m_pressedLast && !pressed) {
224              if (command.isScheduled()) {
225                command.cancel();
226              } else {
227                command.schedule();
228              }
229            }
230
231            m_pressedLast = pressed;
232          }
233        });
234    return this;
235  }
236
237  /**
238   * Starts the given command whenever the trigger just becomes active.
239   *
240   * @param command the command to start
241   * @return this trigger, so calls can be chained
242   * @deprecated Use {@link #onTrue(Command)} instead.
243   */
244  @Deprecated
245  public Trigger whenActive(final Command command) {
246    requireNonNullParam(command, "command", "whenActive");
247
248    m_loop.bind(
249        new Runnable() {
250          private boolean m_pressedLast = m_condition.getAsBoolean();
251
252          @Override
253          public void run() {
254            boolean pressed = m_condition.getAsBoolean();
255
256            if (!m_pressedLast && pressed) {
257              command.schedule();
258            }
259
260            m_pressedLast = pressed;
261          }
262        });
263    return this;
264  }
265
266  /**
267   * Runs the given runnable whenever the trigger just becomes active.
268   *
269   * @param toRun the runnable to run
270   * @param requirements the required subsystems
271   * @return this trigger, so calls can be chained
272   * @deprecated Replace with {@link #onTrue(Command)}, creating the InstantCommand manually
273   */
274  @Deprecated
275  public Trigger whenActive(final Runnable toRun, Subsystem... requirements) {
276    return whenActive(new InstantCommand(toRun, requirements));
277  }
278
279  /**
280   * Constantly starts the given command while the button is held.
281   *
282   * <p>{@link Command#schedule()} will be called repeatedly while the trigger is active, and will
283   * be canceled when the trigger becomes inactive.
284   *
285   * @param command the command to start
286   * @return this trigger, so calls can be chained
287   * @deprecated Use {@link #whileTrue(Command)} with {@link
288   *     edu.wpi.first.wpilibj2.command.RepeatCommand RepeatCommand}, or bind {@link
289   *     Command#schedule() command::schedule} to {@link BooleanEvent#ifHigh(Runnable)} (passing no
290   *     requirements).
291   */
292  @Deprecated
293  public Trigger whileActiveContinuous(final Command command) {
294    requireNonNullParam(command, "command", "whileActiveContinuous");
295
296    m_loop.bind(
297        new Runnable() {
298          private boolean m_pressedLast = m_condition.getAsBoolean();
299
300          @Override
301          public void run() {
302            boolean pressed = m_condition.getAsBoolean();
303
304            if (pressed) {
305              command.schedule();
306            } else if (m_pressedLast) {
307              command.cancel();
308            }
309
310            m_pressedLast = pressed;
311          }
312        });
313
314    return this;
315  }
316
317  /**
318   * Constantly runs the given runnable while the button is held.
319   *
320   * @param toRun the runnable to run
321   * @param requirements the required subsystems
322   * @return this trigger, so calls can be chained
323   * @deprecated Use {@link #whileTrue(Command)} and construct a RunCommand manually
324   */
325  @Deprecated
326  public Trigger whileActiveContinuous(final Runnable toRun, Subsystem... requirements) {
327    return whileActiveContinuous(new InstantCommand(toRun, requirements));
328  }
329
330  /**
331   * Starts the given command when the trigger initially becomes active, and ends it when it becomes
332   * inactive, but does not re-start it in-between.
333   *
334   * @param command the command to start
335   * @return this trigger, so calls can be chained
336   * @deprecated Use {@link #whileTrue(Command)} instead.
337   */
338  @Deprecated
339  public Trigger whileActiveOnce(final Command command) {
340    requireNonNullParam(command, "command", "whileActiveOnce");
341
342    m_loop.bind(
343        new Runnable() {
344          private boolean m_pressedLast = m_condition.getAsBoolean();
345
346          @Override
347          public void run() {
348            boolean pressed = m_condition.getAsBoolean();
349
350            if (!m_pressedLast && pressed) {
351              command.schedule();
352            } else if (m_pressedLast && !pressed) {
353              command.cancel();
354            }
355
356            m_pressedLast = pressed;
357          }
358        });
359    return this;
360  }
361
362  /**
363   * Starts the command when the trigger becomes inactive.
364   *
365   * @param command the command to start
366   * @return this trigger, so calls can be chained
367   * @deprecated Use {@link #onFalse(Command)} instead.
368   */
369  @Deprecated
370  public Trigger whenInactive(final Command command) {
371    requireNonNullParam(command, "command", "whenInactive");
372
373    m_loop.bind(
374        new Runnable() {
375          private boolean m_pressedLast = m_condition.getAsBoolean();
376
377          @Override
378          public void run() {
379            boolean pressed = m_condition.getAsBoolean();
380
381            if (m_pressedLast && !pressed) {
382              command.schedule();
383            }
384
385            m_pressedLast = pressed;
386          }
387        });
388
389    return this;
390  }
391
392  /**
393   * Runs the given runnable when the trigger becomes inactive.
394   *
395   * @param toRun the runnable to run
396   * @param requirements the required subsystems
397   * @return this trigger, so calls can be chained
398   * @deprecated Construct the InstantCommand manually and replace with {@link #onFalse(Command)}
399   */
400  @Deprecated
401  public Trigger whenInactive(final Runnable toRun, Subsystem... requirements) {
402    return whenInactive(new InstantCommand(toRun, requirements));
403  }
404
405  /**
406   * Toggles a command when the trigger becomes active.
407   *
408   * @param command the command to toggle
409   * @return this trigger, so calls can be chained
410   * @deprecated Use {@link #toggleOnTrue(Command)} instead.
411   */
412  @Deprecated
413  public Trigger toggleWhenActive(final Command command) {
414    requireNonNullParam(command, "command", "toggleWhenActive");
415
416    m_loop.bind(
417        new Runnable() {
418          private boolean m_pressedLast = m_condition.getAsBoolean();
419
420          @Override
421          public void run() {
422            boolean pressed = m_condition.getAsBoolean();
423
424            if (!m_pressedLast && pressed) {
425              if (command.isScheduled()) {
426                command.cancel();
427              } else {
428                command.schedule();
429              }
430            }
431
432            m_pressedLast = pressed;
433          }
434        });
435
436    return this;
437  }
438
439  /**
440   * Cancels a command when the trigger becomes active.
441   *
442   * @param command the command to cancel
443   * @return this trigger, so calls can be chained
444   * @deprecated Instead, pass this as an end condition to {@link Command#until(BooleanSupplier)}.
445   */
446  @Deprecated
447  public Trigger cancelWhenActive(final Command command) {
448    requireNonNullParam(command, "command", "cancelWhenActive");
449
450    m_loop.bind(
451        new Runnable() {
452          private boolean m_pressedLast = m_condition.getAsBoolean();
453
454          @Override
455          public void run() {
456            boolean pressed = m_condition.getAsBoolean();
457
458            if (!m_pressedLast && pressed) {
459              command.cancel();
460            }
461
462            m_pressedLast = pressed;
463          }
464        });
465
466    return this;
467  }
468
469  @Override
470  public boolean getAsBoolean() {
471    return m_condition.getAsBoolean();
472  }
473
474  /**
475   * Composes two triggers with logical AND.
476   *
477   * @param trigger the condition to compose with
478   * @return A trigger which is active when both component triggers are active.
479   */
480  public Trigger and(BooleanSupplier trigger) {
481    return new Trigger(() -> m_condition.getAsBoolean() && trigger.getAsBoolean());
482  }
483
484  /**
485   * Composes two triggers with logical OR.
486   *
487   * @param trigger the condition to compose with
488   * @return A trigger which is active when either component trigger is active.
489   */
490  public Trigger or(BooleanSupplier trigger) {
491    return new Trigger(() -> m_condition.getAsBoolean() || trigger.getAsBoolean());
492  }
493
494  /**
495   * Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the
496   * negation of this trigger.
497   *
498   * @return the negated trigger
499   */
500  public Trigger negate() {
501    return new Trigger(() -> !m_condition.getAsBoolean());
502  }
503
504  /**
505   * Creates a new debounced trigger from this trigger - it will become active when this trigger has
506   * been active for longer than the specified period.
507   *
508   * @param seconds The debounce period.
509   * @return The debounced trigger (rising edges debounced only)
510   */
511  public Trigger debounce(double seconds) {
512    return debounce(seconds, Debouncer.DebounceType.kRising);
513  }
514
515  /**
516   * Creates a new debounced trigger from this trigger - it will become active when this trigger has
517   * been active for longer than the specified period.
518   *
519   * @param seconds The debounce period.
520   * @param type The debounce type.
521   * @return The debounced trigger.
522   */
523  public Trigger debounce(double seconds, Debouncer.DebounceType type) {
524    return new Trigger(
525        new BooleanSupplier() {
526          final Debouncer m_debouncer = new Debouncer(seconds, type);
527
528          @Override
529          public boolean getAsBoolean() {
530            return m_debouncer.calculate(m_condition.getAsBoolean());
531          }
532        });
533  }
534}