1 /*
2 * *************************************************************************************************************************************************************
3 *
4 * SteelBlue: DCI User Interfaces
5 * http://tidalwave.it/projects/steelblue
6 *
7 * Copyright (C) 2015 - 2024 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.role.ui.javafx.impl.common;
27
28 import javax.annotation.Nonnull;
29 import javax.annotation.Nullable;
30 import java.util.List;
31 import java.util.concurrent.Executor;
32 import javafx.collections.ObservableList;
33 import javafx.scene.control.Cell;
34 import javafx.scene.control.ContextMenu;
35 import javafx.scene.control.MenuItem;
36 import javafx.scene.input.KeyCode;
37 import it.tidalwave.ui.role.javafx.CustomGraphicProvider;
38 import it.tidalwave.util.As;
39 import it.tidalwave.util.annotation.VisibleForTesting;
40 import it.tidalwave.role.ui.Displayable;
41 import it.tidalwave.role.ui.UserAction;
42 import it.tidalwave.role.ui.UserActionProvider;
43 import lombok.RequiredArgsConstructor;
44 import lombok.extern.slf4j.Slf4j;
45 import static it.tidalwave.ui.role.javafx.CustomGraphicProvider._CustomGraphicProvider_;
46 import static java.util.stream.Collectors.*;
47 import static it.tidalwave.role.ui.Displayable._Displayable_;
48 import static it.tidalwave.role.ui.Styleable._Styleable_;
49 import static it.tidalwave.role.ui.UserActionProvider._UserActionProvider_;
50
51 /***************************************************************************************************************************************************************
52 *
53 * An implementation of {@link CellBinder} that extracts information from a {@link UserActionProvider}.
54 *
55 * @author Fabrizio Giudici
56 *
57 **************************************************************************************************************************************************************/
58 @RequiredArgsConstructor @Slf4j
59 public class DefaultCellBinder implements CellBinder
60 {
61 /** Roles to preload, so they are computed in the background thread. */
62 private static final List<Class<?>> PRELOADING_ROLE_TYPES = List.of(
63 _Displayable_, _UserActionProvider_, _Styleable_, _CustomGraphicProvider_);
64
65 private static final String ROLE_STYLE_PREFIX = "-rs-";
66
67 @Nonnull
68 private final Executor executor;
69
70 /***********************************************************************************************************************************************************
71 * {@inheritDoc}
72 **********************************************************************************************************************************************************/
73 @Override
74 public void bind (@Nonnull final Cell<?> cell, @Nullable final As item, final boolean empty)
75 {
76 log.trace("bind({}, {}, {})", cell, item, empty);
77 clearBindings(cell);
78
79 if (!empty && (item != null))
80 {
81 JavaFXWorker.run(executor,
82 () -> new RoleBag(item, PRELOADING_ROLE_TYPES),
83 roles -> bindAll(cell, roles));
84 }
85 }
86
87 /***********************************************************************************************************************************************************
88 * Binds everything provided by the given {@link RoleBag} to the given {@link Cell}.
89 *
90 * @param cell the {@code Cell}
91 * @param roles the role bag
92 **********************************************************************************************************************************************************/
93 private void bindAll (@Nonnull final Cell<?> cell, @Nonnull final RoleBag roles)
94 {
95 bindTextAndGraphic(cell, roles);
96 bindDefaultAction(cell, roles);
97 bindContextMenu(cell, roles);
98 bindStyles(cell.getStyleClass(), roles);
99 }
100
101 /***********************************************************************************************************************************************************
102 * Binds the text and eventual custom {@link javafx.scene.Node} provided by the given {@link RoleBag} to the given
103 * {@link Cell}.
104 *
105 * @param cell the {@code Cell}
106 * @param roles the role bag
107 **********************************************************************************************************************************************************/
108 private void bindTextAndGraphic (@Nonnull final Cell<?> cell, @Nonnull final RoleBag roles)
109 {
110 final var cgp = roles.get(_CustomGraphicProvider_);
111 cell.setGraphic(cgp.map(CustomGraphicProvider::getGraphic).orElse(null));
112 cell.setText(cgp.map(c -> "").orElse(roles.get(_Displayable_).map(Displayable::getDisplayName).orElse("")));
113 }
114
115 /***********************************************************************************************************************************************************
116 * Binds the default {@link UserAction}s provided by the given {@link RoleBag} as the default action of the given
117 * {@link Cell} (activated by double click or key pressure).
118 *
119 * @param cell the {@code Cell}
120 * @param roles the role bag
121 **********************************************************************************************************************************************************/
122 private void bindDefaultAction (@Nonnull final Cell<?> cell, @Nonnull final RoleBag roles)
123 {
124 roles.getDefaultUserAction().ifPresent(defaultAction ->
125 {
126 // FIXME: doesn't work - keyevents are probably handled by ListView
127 cell.setOnKeyPressed(event ->
128 {
129 log.debug("onKeyPressed: {}", event);
130
131 if (event.getCode().equals(KeyCode.SPACE))
132 {
133 executor.execute(defaultAction::actionPerformed);
134 }
135 });
136
137 // FIXME: depends on mouse click, won't handle keyboard
138 cell.setOnMouseClicked(event ->
139 {
140 if (event.getClickCount() == 2)
141 {
142 executor.execute(defaultAction::actionPerformed);
143 }
144 });
145 });
146 }
147
148 /***********************************************************************************************************************************************************
149 * Binds the {@link UserAction}s provided by the given {@link RoleBag} as items of the contextual menu of a
150 * {@link Cell}.
151 *
152 * @param cell the {@code Cell}
153 * @param roles the role bag
154 **********************************************************************************************************************************************************/
155 private void bindContextMenu (@Nonnull final Cell<?> cell, @Nonnull final RoleBag roles)
156 {
157 final var menuItems = createMenuItems(roles);
158 cell.setContextMenu(menuItems.isEmpty() ? null : new ContextMenu(menuItems.toArray(new MenuItem[0])));
159 }
160
161 /***********************************************************************************************************************************************************
162 * Adds all the styles provided by the given {@link RoleBag} to a {@link ObservableList} of styles.
163 *
164 * @param styleClasses the destination where to add styles
165 * @param roles the role bag
166 **********************************************************************************************************************************************************/
167 @Nonnull
168 private void bindStyles (@Nonnull final ObservableList<String> styleClasses, @Nonnull final RoleBag roles)
169 {
170 final var styles = styleClasses.stream()
171 .filter(s -> !s.startsWith(ROLE_STYLE_PREFIX))
172 .collect(toList());
173 // FIXME: shouldn't reset them? In case of cell reuse, they get accumulated
174 styles.addAll(roles.getMany(_Styleable_)
175 .stream()
176 .flatMap(styleable -> styleable.getStyles().stream())
177 .map(s -> ROLE_STYLE_PREFIX + s)
178 .collect(toList()));
179 styleClasses.setAll(styles);
180 }
181
182 /***********************************************************************************************************************************************************
183 * Create a list of {@link MenuItem}s for each action provided by the given {@link RoleBag}.
184 * Don't directly return a ContextMenu otherwise it will be untestable.
185 *
186 * @param roles the role bag
187 * @return the list of {@MenuItem}s
188 **********************************************************************************************************************************************************/
189 @Nonnull
190 @VisibleForTesting public List<MenuItem> createMenuItems (@Nonnull final RoleBag roles)
191 {
192 return roles.getMany(_UserActionProvider_).stream()
193 .flatMap(uap -> uap.getActions().stream())
194 .map(this::createMenuItem)
195 .collect(toList());
196 }
197
198 /***********************************************************************************************************************************************************
199 *
200 **********************************************************************************************************************************************************/
201 private void clearBindings (@Nonnull final Cell<?> cell)
202 {
203 cell.setText("");
204 cell.setGraphic(null);
205 cell.setContextMenu(null);
206 cell.setOnKeyPressed(null);
207 cell.setOnMouseClicked(null);
208 }
209
210 /***********************************************************************************************************************************************************
211 * Creates a {@link MenuItem} bound to the given action.
212 *
213 * @param action the action
214 * @return the bound {@code MenuItem}
215 **********************************************************************************************************************************************************/
216 @Nonnull
217 private MenuItem createMenuItem (@Nonnull final UserAction action)
218 {
219 final var menuItem = new MenuItem(action.as(_Displayable_).getDisplayName());
220 menuItem.setOnAction(new EventHandlerUserActionAdapter(executor, action));
221 return menuItem;
222 }
223 }