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.role.ui.javafx.impl;
27
28 import javax.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 javafx.beans.binding.BooleanExpression;
34 import javafx.beans.property.Property;
35 import javafx.geometry.HPos;
36 import javafx.geometry.VPos;
37 import javafx.scene.control.Button;
38 import javafx.scene.control.ButtonBase;
39 import javafx.scene.control.MenuItem;
40 import javafx.scene.control.TextField;
41 import javafx.scene.control.ToggleButton;
42 import javafx.scene.control.ToggleGroup;
43 import javafx.scene.layout.ColumnConstraints;
44 import javafx.scene.layout.GridPane;
45 import javafx.scene.layout.Pane;
46 import javafx.scene.layout.Priority;
47 import javafx.stage.Window;
48 import javafx.application.Platform;
49 import it.tidalwave.role.SimpleComposite;
50 import it.tidalwave.role.ui.BoundProperty;
51 import it.tidalwave.role.ui.Displayable;
52 import it.tidalwave.role.ui.PresentationModel;
53 import it.tidalwave.role.ui.Styleable;
54 import it.tidalwave.role.ui.UserAction;
55 import it.tidalwave.role.ui.UserActionProvider;
56 import it.tidalwave.role.ui.javafx.JavaFXBinder;
57 import it.tidalwave.role.ui.javafx.impl.combobox.ComboBoxBindings;
58 import it.tidalwave.role.ui.javafx.impl.common.CellBinder;
59 import it.tidalwave.role.ui.javafx.impl.common.ChangeListenerSelectableAdapter;
60 import it.tidalwave.role.ui.javafx.impl.common.DefaultCellBinder;
61 import it.tidalwave.role.ui.javafx.impl.common.PropertyAdapter;
62 import it.tidalwave.role.ui.javafx.impl.dialog.DialogBindings;
63 import it.tidalwave.role.ui.javafx.impl.filechooser.FileChooserBindings;
64 import it.tidalwave.role.ui.javafx.impl.list.ListViewBindings;
65 import it.tidalwave.role.ui.javafx.impl.tableview.TableViewBindings;
66 import it.tidalwave.role.ui.javafx.impl.tree.TreeViewBindings;
67 import it.tidalwave.role.ui.javafx.impl.treetable.TreeTableViewBindings;
68 import lombok.experimental.Delegate;
69 import lombok.extern.slf4j.Slf4j;
70 import static java.util.Collections.emptyList;
71 import static java.util.Objects.requireNonNull;
72 import static java.util.stream.Collectors.*;
73 import static it.tidalwave.role.SimpleComposite._SimpleComposite_;
74 import static it.tidalwave.role.ui.Displayable._Displayable_;
75 import static it.tidalwave.role.ui.Styleable._Styleable_;
76 import static it.tidalwave.role.ui.UserActionProvider._UserActionProvider_;
77
78
79
80
81
82
83 @Slf4j
84 public class DefaultJavaFXBinder implements JavaFXBinder
85 {
86 private final Executor executor;
87
88 private final String invalidTextFieldStyle = "-fx-background-color: pink";
89
90 interface Exclusions
91 {
92 public void setMainWindow (Window window);
93
94 public ChangeListenerSelectableAdapter getSelectionListener();
95 }
96
97 @Delegate(excludes = Exclusions.class)
98 private final TreeViewBindings treeItemBindings;
99
100 @Delegate(excludes = Exclusions.class)
101 private final TableViewBindings tableViewBindings;
102
103 @Delegate(excludes = Exclusions.class)
104 private final TreeTableViewBindings treeTableViewBindings;
105
106 @Delegate(excludes = Exclusions.class)
107 private final ListViewBindings listViewBindings;
108
109 @Delegate(excludes = Exclusions.class)
110 private final ComboBoxBindings comboBoxBindings;
111
112 @Delegate(excludes = Exclusions.class)
113 private final DialogBindings dialogBindings;
114
115 @Delegate(excludes = Exclusions.class)
116 private final FileChooserBindings fileChooserBindings;
117
118 private final CellBinder cellBinder;
119
120
121
122
123 public DefaultJavaFXBinder (@Nonnull final Executor executor)
124 {
125 this.executor = executor;
126 cellBinder = new DefaultCellBinder(executor);
127 comboBoxBindings = new ComboBoxBindings(executor, cellBinder);
128 treeItemBindings = new TreeViewBindings(executor, cellBinder);
129 tableViewBindings = new TableViewBindings(executor, cellBinder);
130 treeTableViewBindings = new TreeTableViewBindings(executor, cellBinder);
131 listViewBindings = new ListViewBindings(executor, cellBinder);
132 dialogBindings = new DialogBindings(executor);
133 fileChooserBindings = new FileChooserBindings(executor);
134 }
135
136
137
138
139 @Override
140 public void setMainWindow (@Nonnull final Window mainWindow)
141 {
142 treeItemBindings.setMainWindow(mainWindow);
143 tableViewBindings.setMainWindow(mainWindow);
144 dialogBindings.setMainWindow(mainWindow);
145 fileChooserBindings.setMainWindow(mainWindow);
146 }
147
148
149
150
151 @Override
152 public void bind (@Nonnull final ButtonBase button, @Nonnull final UserAction action)
153 {
154 assertIsFxApplicationThread();
155 button.setText(action.maybeAs(_Displayable_).map(Displayable::getDisplayName).orElse(""));
156 button.disableProperty().bind(adaptBoolean(action.enabled()).not());
157 button.setOnAction(__ -> executor.execute(action::actionPerformed));
158 }
159
160
161
162
163 @Override
164 public void bind (@Nonnull final MenuItem menuItem, @Nonnull final UserAction action)
165 {
166 assertIsFxApplicationThread();
167 menuItem.setText(action.maybeAs(_Displayable_).map(Displayable::getDisplayName).orElse(""));
168 menuItem.disableProperty().bind(adaptBoolean(action.enabled()).not());
169 menuItem.setOnAction(__ -> executor.execute(action::actionPerformed));
170 }
171
172
173
174
175 @Override
176 public <T> void bindBidirectionally (@Nonnull final Property<T> property1, @Nonnull final BoundProperty<T> property2)
177 {
178 assertIsFxApplicationThread();
179 property1.bindBidirectional(new PropertyAdapter<>(executor, property2));
180 }
181
182
183
184
185 @Override
186 public <T> void bindBidirectionally (@Nonnull final TextField textField,
187 @Nonnull final BoundProperty<String> textProperty,
188 @Nonnull final BoundProperty<Boolean> validProperty)
189 {
190 assertIsFxApplicationThread();
191 requireNonNull(textField, "textField");
192 requireNonNull(textProperty, "textProperty");
193 requireNonNull(validProperty, "validProperty");
194
195 textField.textProperty().bindBidirectional(new PropertyAdapter<>(executor, textProperty));
196
197
198 validProperty.addPropertyChangeListener(__ -> textField.setStyle(validProperty.get() ? "" : invalidTextFieldStyle));
199 }
200
201
202
203
204 @Override
205 public void bindToggleButtons (@Nonnull final Pane pane, @Nonnull final PresentationModel pm)
206 {
207 assertIsFxApplicationThread();
208 final var group = new ToggleGroup();
209 final var children = pane.getChildren();
210 final var prototypeStyleClass = children.get(0).getStyleClass();
211 final SimpleComposite<PresentationModel> pmc = pm.as(_SimpleComposite_);
212 children.setAll(pmc.findChildren().stream().map(cpm -> createToggleButton(cpm, prototypeStyleClass, group)).collect(toList()));
213 }
214
215
216
217
218 @Override
219 public void bindButtonsInPane (@Nonnull final GridPane gridPane, @Nonnull final Collection<UserAction> actions)
220 {
221 assertIsFxApplicationThread();
222 final var columnConstraints = gridPane.getColumnConstraints();
223 final var children = gridPane.getChildren();
224
225 columnConstraints.clear();
226 children.clear();
227 final var columnIndex = new AtomicInteger(0);
228
229 actions.forEach(menuAction ->
230 {
231 final var column = new ColumnConstraints();
232 column.setPercentWidth(100.0 / actions.size());
233 columnConstraints.add(column);
234 final var button = createButton();
235 GridPane.setConstraints(button, columnIndex.getAndIncrement(), 0);
236 bind(button, menuAction);
237 children.add(button);
238 });
239 }
240
241
242
243
244 @Nonnull
245 private Button createButton()
246 {
247 final var button = new Button();
248 GridPane.setHgrow(button, Priority.ALWAYS);
249 GridPane.setVgrow(button, Priority.ALWAYS);
250 GridPane.setHalignment(button, HPos.CENTER);
251 GridPane.setValignment(button, VPos.CENTER);
252 button.setPrefSize(999, 999);
253 button.getStyleClass().add("mainMenuButton");
254
255 return button;
256 }
257
258
259
260
261 @Nonnull
262 private ToggleButton createToggleButton (@Nonnull final PresentationModel pm, @Nonnull final List<String> baseStyleClass, @Nonnull final ToggleGroup group)
263 {
264 final var button = new ToggleButton();
265 button.setToggleGroup(group);
266 button.setText(pm.maybeAs(_Displayable_).map(Displayable::getDisplayName).orElse(""));
267 button.getStyleClass().addAll(baseStyleClass);
268 button.getStyleClass().addAll(pm.maybeAs(_Styleable_).map(Styleable::getStyles).orElse(emptyList()));
269 pm.maybeAs(_UserActionProvider_).flatMap(UserActionProvider::getOptionalDefaultAction)
270 .ifPresent(action -> bind(button, action));
271
272 if (group.getSelectedToggle() == null)
273 {
274 group.selectToggle(button);
275 }
276
277 return button;
278 }
279
280
281
282
283 private void assertIsFxApplicationThread()
284 {
285 if (!Platform.isFxApplicationThread())
286 {
287 throw new AssertionError("Must run in the JavaFX Application Thread");
288 }
289 }
290
291
292
293
294 @Nonnull
295 private BooleanExpression adaptBoolean (@Nonnull final BoundProperty<Boolean> property)
296 {
297 return BooleanExpression.booleanExpression(new PropertyAdapter<>(executor, property));
298 }
299 }