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