View Javadoc
1   /*
2    * *************************************************************************************************************************************************************
3    *
4    * SteelBlue: DCI User Interfaces
5    * http://tidalwave.it/projects/steelblue
6    *
7    * Copyright (C) 2015 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
8    *
9    * *************************************************************************************************************************************************************
10   *
11   * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12   * You may obtain a copy of the License at
13   *
14   *     http://www.apache.org/licenses/LICENSE-2.0
15   *
16   * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
17   * CONDITIONS OF ANY KIND, either express or implied.  See the License for the specific language governing permissions and limitations under the License.
18   *
19   * *************************************************************************************************************************************************************
20   *
21   * git clone https://bitbucket.org/tidalwave/steelblue-src
22   * git clone https://github.com/tidalwave-it/steelblue-src
23   *
24   * *************************************************************************************************************************************************************
25   */
26  package it.tidalwave.ui.javafx.impl;
27  
28  import jakarta.annotation.Nonnull;
29  import java.util.Collection;
30  import java.util.List;
31  import java.util.concurrent.Executor;
32  import java.util.concurrent.atomic.AtomicInteger;
33  import java.util.function.Function;
34  import javafx.beans.property.BooleanProperty;
35  import javafx.beans.property.Property;
36  import javafx.geometry.HPos;
37  import javafx.geometry.VPos;
38  import javafx.scene.control.Button;
39  import javafx.scene.control.ButtonBase;
40  import javafx.scene.control.MenuItem;
41  import javafx.scene.control.TextField;
42  import javafx.scene.control.ToggleButton;
43  import javafx.scene.control.ToggleGroup;
44  import javafx.scene.layout.ColumnConstraints;
45  import javafx.scene.layout.GridPane;
46  import javafx.scene.layout.Pane;
47  import javafx.scene.layout.Priority;
48  import javafx.stage.Window;
49  import javafx.application.Platform;
50  import it.tidalwave.role.SimpleComposite;
51  import it.tidalwave.ui.core.BoundProperty;
52  import it.tidalwave.ui.core.role.Displayable;
53  import it.tidalwave.ui.core.role.PresentationModel;
54  import it.tidalwave.ui.core.role.Styleable;
55  import it.tidalwave.ui.core.role.UserAction;
56  import it.tidalwave.ui.core.role.UserActionProvider;
57  import it.tidalwave.ui.javafx.JavaFXBinder;
58  import it.tidalwave.ui.javafx.impl.combobox.ComboBoxBindings;
59  import it.tidalwave.ui.javafx.impl.common.CellBinder;
60  import it.tidalwave.ui.javafx.impl.common.DefaultCellBinder;
61  import it.tidalwave.ui.javafx.impl.common.PropertyAdapter;
62  import it.tidalwave.ui.javafx.impl.dialog.DialogBindings;
63  import it.tidalwave.ui.javafx.impl.filechooser.FileChooserBindings;
64  import it.tidalwave.ui.javafx.impl.list.ListViewBindings;
65  import it.tidalwave.ui.javafx.impl.tableview.TableViewBindings;
66  import it.tidalwave.ui.javafx.impl.tree.TreeViewBindings;
67  import it.tidalwave.ui.javafx.impl.treetable.TreeTableViewBindings;
68  import it.tidalwave.util.As;
69  import lombok.experimental.Delegate;
70  import lombok.extern.slf4j.Slf4j;
71  import static java.util.Collections.emptyList;
72  import static java.util.Objects.requireNonNull;
73  import static java.util.stream.Collectors.*;
74  import static it.tidalwave.ui.core.role.Displayable._Displayable_;
75  import static it.tidalwave.ui.core.role.Styleable._Styleable_;
76  import static it.tidalwave.ui.core.role.UserActionProvider._UserActionProvider_;
77  
78  /***************************************************************************************************************************************************************
79   *
80   * @author  Fabrizio Giudici
81   *
82   **************************************************************************************************************************************************************/
83  @Slf4j
84  public class DefaultJavaFXBinder implements JavaFXBinder
85    {
86      private static final As.Type<SimpleComposite<PresentationModel>>  _SimpleCompositePresentationModel_ = new As.Type<>(SimpleComposite.class);
87  
88      private final Executor executor;
89  
90      private final String invalidTextFieldStyle = "-fx-background-color: pink";
91  
92      @Delegate
93      private final TreeViewBindings treeItemBindings;
94  
95      @Delegate
96      private final TableViewBindings tableViewBindings;
97  
98      @Delegate
99      private final TreeTableViewBindings treeTableViewBindings;
100 
101     @Delegate
102     private final ListViewBindings listViewBindings;
103 
104     @Delegate
105     private final ComboBoxBindings comboBoxBindings;
106 
107     @Delegate
108     private final DialogBindings dialogBindings;
109 
110     @Delegate
111     private final FileChooserBindings fileChooserBindings;
112 
113     private final CellBinder cellBinder;
114 
115     /***********************************************************************************************************************************************************
116      *
117      **********************************************************************************************************************************************************/
118     public DefaultJavaFXBinder (@Nonnull final Executor executor)
119       {
120         this.executor = executor;
121         cellBinder = new DefaultCellBinder(executor);
122         comboBoxBindings = new ComboBoxBindings(executor, cellBinder);
123         treeItemBindings = new TreeViewBindings(executor, cellBinder);
124         tableViewBindings = new TableViewBindings(executor, cellBinder);
125         treeTableViewBindings = new TreeTableViewBindings(executor, cellBinder);
126         listViewBindings = new ListViewBindings(executor, cellBinder);
127         dialogBindings = new DialogBindings(executor);
128         fileChooserBindings = new FileChooserBindings(executor);
129       }
130 
131     /***********************************************************************************************************************************************************
132      * {@inheritDoc}
133      **********************************************************************************************************************************************************/
134     @Override
135     public void setMainWindow (@Nonnull final Window mainWindow)
136       {
137         treeItemBindings.setMainWindow(mainWindow);
138         tableViewBindings.setMainWindow(mainWindow);
139         dialogBindings.setMainWindow(mainWindow);
140         fileChooserBindings.setMainWindow(mainWindow);
141       }
142 
143     /***********************************************************************************************************************************************************
144      * {@inheritDoc}
145      **********************************************************************************************************************************************************/
146     @Override
147     public void bind (@Nonnull final ButtonBase button, @Nonnull final UserAction action)
148       {
149         enforceFxApplicationThread();
150         action.maybeAs(_Displayable_).ifPresent(d -> button.setText(d.getDisplayName()));
151         button.setOnAction(__ -> executor.execute(action::actionPerformed));
152         bindEnableProperty(button.disableProperty(), action.enabled());
153       }
154 
155     /***********************************************************************************************************************************************************
156      * {@inheritDoc}
157      **********************************************************************************************************************************************************/
158     @Override
159     public void bind (@Nonnull final MenuItem menuItem, @Nonnull final UserAction action)
160       {
161         enforceFxApplicationThread();
162         menuItem.setText(action.maybeAs(_Displayable_).map(Displayable::getDisplayName).orElse(""));
163         menuItem.setOnAction(__ -> executor.execute(action::actionPerformed));
164         bindEnableProperty(menuItem.disableProperty(), action.enabled());
165       }
166 
167     /***********************************************************************************************************************************************************
168      * {@inheritDoc}
169      **********************************************************************************************************************************************************/
170     @Override
171     public <T, S> void bind (@Nonnull final BoundProperty<? super T> target,
172                              @Nonnull final Property<? extends S> source,
173                              @Nonnull final Function<S, T> adapter)
174       {
175         enforceFxApplicationThread();
176         source.addListener((_1, _2, newValue) -> executor.execute(() -> target.set(adapter.apply(newValue))));
177       }
178 
179     /***********************************************************************************************************************************************************
180      * {@inheritDoc}
181      **********************************************************************************************************************************************************/
182     @Override @SuppressWarnings("unchecked")
183     public <T, S> void bindBidirectionally (@Nonnull final BoundProperty<? super T> property1,
184                                             @Nonnull final Property<S> property2,
185                                             @Nonnull final Function<? super S, T> adapter,
186                                             @Nonnull final Function<? super T, ? extends S> reverseAdapter)
187       {
188         enforceFxApplicationThread();
189         property2.addListener((_1, _2, newValue) -> executor.execute(() -> property1.set(adapter.apply(newValue))));
190         property1.addPropertyChangeListener(evt -> Platform.runLater(() -> property2.setValue(reverseAdapter.apply((T)evt.getNewValue()))));
191       }
192 
193     /***********************************************************************************************************************************************************
194      * {@inheritDoc}
195      **********************************************************************************************************************************************************/
196     @Override
197     public void bindBidirectionally (@Nonnull final TextField textField,
198                                      @Nonnull final BoundProperty<String> textProperty,
199                                      @Nonnull final BoundProperty<Boolean> validProperty)
200       {
201         enforceFxApplicationThread();
202         requireNonNull(textField, "textField");
203         requireNonNull(textProperty, "textProperty");
204         requireNonNull(validProperty, "validProperty");
205 
206         textField.textProperty().bindBidirectional(new PropertyAdapter<>(executor, textProperty));
207 
208         // FIXME: weak listener
209         validProperty.addPropertyChangeListener(__ -> textField.setStyle(validProperty.get() ? "" : invalidTextFieldStyle));
210       }
211 
212     /***********************************************************************************************************************************************************
213      * {@inheritDoc}
214      **********************************************************************************************************************************************************/
215     @Override
216     public void bindToggleButtons (@Nonnull final Pane pane, @Nonnull final PresentationModel pm)
217       {
218         enforceFxApplicationThread();
219         final var group = new ToggleGroup();
220         final var children = pane.getChildren();
221         final var prototypeStyleClass = children.get(0).getStyleClass();
222         final SimpleComposite<PresentationModel> pmc = pm.as(_SimpleCompositePresentationModel_);
223         children.setAll(pmc.findChildren().stream().map(cpm -> createToggleButton(cpm, prototypeStyleClass, group)).collect(toList()));
224       }
225 
226     /***********************************************************************************************************************************************************
227      *
228      **********************************************************************************************************************************************************/
229     @Override
230     public void bindButtonsInPane (@Nonnull final GridPane gridPane, @Nonnull final Collection<UserAction> actions)
231       {
232         enforceFxApplicationThread();
233         final var columnConstraints = gridPane.getColumnConstraints();
234         final var children = gridPane.getChildren();
235 
236         columnConstraints.clear();
237         children.clear();
238         final var columnIndex = new AtomicInteger(0);
239 
240         actions.forEach(menuAction ->
241           {
242             final var column = new ColumnConstraints();
243             column.setPercentWidth(100.0 / actions.size());
244             columnConstraints.add(column);
245             final var button = createButton();
246             GridPane.setConstraints(button, columnIndex.getAndIncrement(), 0);
247             bind(button, menuAction);
248             children.add(button);
249           });
250       }
251 
252     /***********************************************************************************************************************************************************
253      * {@return a new {@code Button}} for the menu bar.
254      **********************************************************************************************************************************************************/
255     @Nonnull
256     private Button createButton()
257       {
258         final var button = new Button();
259         GridPane.setHgrow(button, Priority.ALWAYS);
260         GridPane.setVgrow(button, Priority.ALWAYS);
261         GridPane.setHalignment(button, HPos.CENTER);
262         GridPane.setValignment(button, VPos.CENTER);
263         button.setPrefSize(999, 999); // fill
264         button.getStyleClass().add("mainMenuButton");
265 
266         return button;
267       }
268 
269     /***********************************************************************************************************************************************************
270      *
271      **********************************************************************************************************************************************************/
272     @Nonnull
273     private ToggleButton createToggleButton (@Nonnull final PresentationModel pm, @Nonnull final List<String> baseStyleClass, @Nonnull final ToggleGroup group)
274       {
275         final var button = new ToggleButton();
276         button.setToggleGroup(group);
277         button.setText(pm.maybeAs(_Displayable_).map(Displayable::getDisplayName).orElse(""));
278         button.getStyleClass().addAll(baseStyleClass);
279         button.getStyleClass().addAll(pm.maybeAs(_Styleable_).map(Styleable::getStyles).orElse(emptyList()));
280         pm.maybeAs(_UserActionProvider_).flatMap(UserActionProvider::getOptionalDefaultAction)
281                                         .ifPresent(action -> bind(button, action));
282 
283         if (group.getSelectedToggle() == null)
284           {
285             group.selectToggle(button);
286           }
287 
288         return button;
289       }
290 
291     /***********************************************************************************************************************************************************
292      *
293      **********************************************************************************************************************************************************/
294     public static void enforceFxApplicationThread()
295       {
296         if (!Platform.isFxApplicationThread())
297           {
298             throw new IllegalStateException("Must run in the JavaFX Application Thread");
299           }
300       }
301 
302     /***********************************************************************************************************************************************************
303      *
304      **********************************************************************************************************************************************************/
305     private void bindEnableProperty (@Nonnull final BooleanProperty property1, @Nonnull final BoundProperty<Boolean> property2)
306       {
307         property1.setValue(!property2.get());
308         property1.addListener((_1, _2, newValue) -> executor.execute(() -> property2.set(!newValue)));
309         property2.addPropertyChangeListener(evt -> Platform.runLater(() -> property1.setValue(!(boolean)evt.getNewValue())));
310       }
311   }