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