<div dir="ltr">I love this idea and would use it immediately!</div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Fri, Mar 24, 2023 at 12:10 AM John Hendrikx <<a href="mailto:john.hendrikx@gmail.com">john.hendrikx@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">Hi list,<br>
<br>
I've been working on a potential new API (and proof of concept <br>
implementation) for adding a new type of fluent binding which can delay <br>
changing their values, and I'm wondering how much interest there is in <br>
such a thing.<br>
<br>
The main purpose of such an API is to prevent being flooded with changes <br>
when properties change often, or to simply delay certain actions until <br>
the user has settled on a selection or has stopped typing.<br>
<br>
For this purpose I would like to introduce a default method on <br>
`ObservableValue` with the signature:<br>
<br>
     ObservableValue<T> throttle(Throttler throttler);<br>
<br>
The parameter `Throttler` can be obtained via static methods of a helper <br>
class named `FXThrottlers`. These provide various pre-configured <br>
throttlers that work correctly with JavaFX's event thread model.  My <br>
current proof of concept provides:<br>
<br>
     public static Throttler debounce(Duration quietPeriod);<br>
     public static Throttler debounceTrailing(Duration quietPeriod);<br>
     public static Throttler throttle(Duration period);<br>
     public static Throttler throttleTrailing(Duration period);<br>
<br>
These are variations of similar concepts, and vary mostly in when <br>
exactly they will allow value changes; debouncers will wait for a period <br>
without any changes, while throttlers will periodically allow changes. <br>
The trailing variants will not immediately emit the first change but <br>
will wait for the period to elapse first; all variants will eventually <br>
take on the value of the source observable.  Debouncing is typically <br>
used when you wish for an input to settle before taking action (like <br>
typing in a search bar), while throttling is used to give regular <br>
feedback but avoid doing so too often (like feedback during window <br>
resizing).<br>
<br>
Usage example which updates a preview panel when the user has finished <br>
(cursor) scrolling through a list view:<br>
<br>
     ObjectProperty<T> selectedItem = <br>
listView.getSelectionModel().selectedItemProperty();<br>
<br>
     selectedItem<br>
.throttle(FXThrottlers.debounceTrailing(Duration.ofMillis(500)))<br>
         .addListener((obs, old, current) -> {<br>
              if (current != null) {<br>
                  updatePreviewPanel(current);<br>
              }<br>
         });<br>
<br>
Implementation details:<br>
<br>
ObservableValue is part of javafx.base, and as such can't use animations <br>
or call Platform::runLater.  The ThrottledBinding implementation has <br>
abstracted all of these out into the Throttler class, and FXThrottlers <br>
(which would live in javafx.graphics) therefore provides the necessary <br>
call backs to integrate property changes correctly back onto the JavaFX <br>
event thread.  The Throttler class also simplifies testing; the test can <br>
provide its own timing source and background scheduler.  The Throttler <br>
interface has the following methods:<br>
<br>
     /**<br>
      * Schedules a command to run on an unspecified thread after the time<br>
      * given by {@code nanos} elapses.<br>
      *<br>
      * @param command a command to run, cannot be {@code null}<br>
      * @param nanos a time in nanoseconds<br>
      */<br>
     void schedule(Runnable command, long nanos);<br>
<br>
     /**<br>
      * Provides the current time in nanoseconds.<br>
      *<br>
      * @return the current time in nanoseconds<br>
      */<br>
     long nanoTime();<br>
<br>
     /**<br>
      * Runs the given command as soon as possible on a thread specified <br>
by this<br>
      * throttler for updating property values.<br>
      *<br>
      * @param command a command to run, cannot be {@code null}<br>
      */<br>
     void update(Runnable command);<br>
<br>
     /**<br>
      * Given the current elapsed time in the current change window, and the<br>
      * amount of time elapsed since the last change was detected, <br>
determines<br>
      * if and by how much the current change window should be extended.<br>
      *<br>
      * @param elapsed nanoseconds elapsed since the start of the <br>
current change window<br>
      * @param elapsedSinceLastChange nanoseconds elapsed since the last <br>
change<br>
      * @return nanoseconds to extend the window with<br>
      */<br>
     long determineInterval(long elapsed, long elapsedSinceLastChange);<br>
<br>
For testing purposes, the schedule and nanoTime can be provided such <br>
that the throttle function can be tested deterministically. For <br>
integrating with JavaFX, update is implemented as <br>
`Platform.runLater(command)`.  The schedule and nanoTime methods <br>
delegate to an Executor and System.nanoTime respectively.  When using <br>
properties without JavaFX, Throttler implementations can be provided <br>
which run property updates on a scheduler thread (just calling <br>
Runnable::run on the current thread) or via some user provided executor.<br>
<br>
A sample test case looks like this (read with a mono space font :-)):<br>
<br>
     @Test<br>
     void testThrottleLeadingAndTrailing() {<br>
       // create Throttler with deterministic behavior:<br>
       Throttler throttler = <br>
create(Throttler.IntervalHandler.throttle(Duration.ofNanos(4));<br>
<br>
       // create throttled observable:<br>
       ObservableValue<String> binding = source.throttle(throttler);<br>
<br>
       assertChanges(<br>
         binding,<br>
         "--a-b--c---d-----e-------f-g-----------f-g-----",<br>
         "--a---b---c---d---e------f---g---------f---g---"<br>
       );<br>
<br>
       assertInvalidations(<br>
         binding,<br>
         "--a-b--c---d-----e-------f-g-----------f-g-----",<br>
         "--i---i---i---i---i------i---i---------i---i---"<br>
       );<br>
     }<br>
<br>
Thanks for reading, I look forward to your feedback!<br>
<br>
--John<br>
<br>
<br>
<br>
<br>
</blockquote></div>