1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
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
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
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
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
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
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
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
209 validProperty.addPropertyChangeListener(__ -> textField.setStyle(validProperty.get() ? "" : invalidTextFieldStyle));
210 }
211
212
213
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
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);
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 }