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 }