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( 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 }