View Javadoc
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   }