DefaultCellBinder.java

  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. import jakarta.annotation.Nonnull;
  28. import jakarta.annotation.Nullable;
  29. import java.util.List;
  30. import java.util.concurrent.Executor;
  31. import javafx.collections.ObservableList;
  32. import javafx.scene.control.Cell;
  33. import javafx.scene.control.ContextMenu;
  34. import javafx.scene.control.MenuItem;
  35. import javafx.scene.input.KeyCode;
  36. import it.tidalwave.ui.core.role.Displayable;
  37. import it.tidalwave.ui.core.role.UserAction;
  38. import it.tidalwave.ui.core.role.UserActionProvider;
  39. import it.tidalwave.ui.javafx.role.CustomGraphicProvider;
  40. import it.tidalwave.util.As;
  41. import it.tidalwave.util.annotation.VisibleForTesting;
  42. import lombok.RequiredArgsConstructor;
  43. import lombok.extern.slf4j.Slf4j;
  44. import static it.tidalwave.ui.core.role.Displayable._Displayable_;
  45. import static it.tidalwave.ui.core.role.Styleable._Styleable_;
  46. import static it.tidalwave.ui.core.role.UserActionProvider._UserActionProvider_;
  47. import static it.tidalwave.ui.javafx.role.CustomGraphicProvider._CustomGraphicProvider_;
  48. import static java.util.stream.Collectors.*;

  49. /***************************************************************************************************************************************************************
  50.  *
  51.  * An implementation of {@link CellBinder} that extracts information from a {@link UserActionProvider}.
  52.  *
  53.  * @author  Fabrizio Giudici
  54.  *
  55.  **************************************************************************************************************************************************************/
  56. @RequiredArgsConstructor @Slf4j
  57. public class DefaultCellBinder implements CellBinder
  58.   {
  59.     /** Roles to preload, so they are computed in the background thread. */
  60.     private static final List<Class<?>> PRELOADING_ROLE_TYPES = List.of(_Displayable_, _UserActionProvider_, _Styleable_, _CustomGraphicProvider_);

  61.     private static final String ROLE_STYLE_PREFIX = "-rs-";

  62.     @Nonnull
  63.     private final Executor executor;

  64.     /***********************************************************************************************************************************************************
  65.      * {@inheritDoc}
  66.      **********************************************************************************************************************************************************/
  67.     @Override
  68.     public void bind (@Nonnull final Cell<?> cell, @Nullable final As item, final boolean empty)
  69.       {
  70.         log.trace("bind({}, {}, {})", cell, item, empty);

  71.         if (!empty && (item != null))
  72.           {
  73.             JavaFXWorker.run(executor,
  74.                              () -> new RoleCollector(item, PRELOADING_ROLE_TYPES),
  75.                              roles -> bindAll(cell, roles));
  76.           }
  77.         else
  78.           {
  79.             cell.setGraphic(null);
  80.             cell.setText("");
  81.             cell.setOnKeyPressed(null);
  82.             cell.setOnMouseClicked(null);
  83.             cell.setContextMenu(null);
  84.             // cell.getStyleClass().clear();
  85.           }
  86.       }

  87.     /***********************************************************************************************************************************************************
  88.      * Binds everything provided by the given {@link RoleCollector} to the given {@link Cell}.
  89.      * @param     cell            the {@code Cell}
  90.      * @param     roles           the role bag
  91.      **********************************************************************************************************************************************************/
  92.     private void bindAll (@Nonnull final Cell<?> cell, @Nonnull final RoleCollector roles)
  93.       {
  94.         bindTextAndGraphic(cell, roles);
  95.         bindDefaultAction(cell, roles);
  96.         bindContextMenu(cell, roles);
  97.         bindStyles(cell.getStyleClass(), roles);
  98.       }

  99.     /***********************************************************************************************************************************************************
  100.      * Binds the text and eventual custom {@link javafx.scene.Node} provided by the given {@link RoleCollector} to the given {@link Cell}.
  101.      * @param     cell            the {@code Cell}
  102.      * @param     roles           the role bag
  103.      **********************************************************************************************************************************************************/
  104.     private void bindTextAndGraphic (@Nonnull final Cell<?> cell, @Nonnull final RoleCollector roles)
  105.       {
  106.         final var cgp = roles.get(_CustomGraphicProvider_);
  107.         final var graphic = cgp.map(CustomGraphicProvider::getGraphic).orElse(null);
  108.         final var text = cgp.map(c -> "").orElse(roles.get(_Displayable_).map(Displayable::getDisplayName).orElse(""));
  109.         log.trace("bindTextAndGraphic({}, {}) - graphic: {}, text: {}", cell, roles, graphic, text);
  110.         cell.setGraphic(graphic);
  111.         cell.setText(text);
  112.       }

  113.     /***********************************************************************************************************************************************************
  114.      * Binds the default {@link UserAction}s provided by the given {@link RoleCollector} as the default action of the given {@link Cell} (activated by double
  115.      * click or key pressure).
  116.      * @param     cell            the {@code Cell}
  117.      * @param     roles           the role bag
  118.      **********************************************************************************************************************************************************/
  119.     private void bindDefaultAction (@Nonnull final Cell<?> cell, @Nonnull final RoleCollector roles)
  120.       {
  121.         roles.getDefaultUserAction().ifPresent(defaultAction ->
  122.           {
  123.             // FIXME: doesn't work - keyevents are probably handled by ListView
  124.             cell.setOnKeyPressed(event ->
  125.               {
  126.                 log.debug("onKeyPressed: {}", event);

  127.                 if (event.getCode().equals(KeyCode.SPACE))
  128.                   {
  129.                     executor.execute(defaultAction::actionPerformed);
  130.                   }
  131.               });

  132.             // FIXME: depends on mouse click, won't handle keyboard
  133.             cell.setOnMouseClicked(event ->
  134.               {
  135.                 if (event.getClickCount() == 2)
  136.                   {
  137.                     executor.execute(defaultAction::actionPerformed);
  138.                   }
  139.               });
  140.           });
  141.       }

  142.     /***********************************************************************************************************************************************************
  143.      * Binds the {@link UserAction}s provided by the given {@link RoleCollector} as items of the contextual menu of a {@link Cell}.
  144.      * @param     cell            the {@code Cell}
  145.      * @param     roles           the role bag
  146.      **********************************************************************************************************************************************************/
  147.     private void bindContextMenu (@Nonnull final Cell<?> cell, @Nonnull final RoleCollector roles)
  148.       {
  149.         final var menuItems = createMenuItems(roles);
  150.         cell.setContextMenu(menuItems.isEmpty() ? null : new ContextMenu(menuItems.toArray(new MenuItem[0])));
  151.       }

  152.     /***********************************************************************************************************************************************************
  153.      * Adds all the styles provided by the given {@link RoleCollector} to a {@link ObservableList} of styles.
  154.      * @param     styleClasses    the destination where to add styles
  155.      * @param     roles           the role bag
  156.      **********************************************************************************************************************************************************/
  157.     @Nonnull
  158.     private void bindStyles (@Nonnull final ObservableList<String> styleClasses, @Nonnull final RoleCollector roles)
  159.       {
  160.         final var styles = styleClasses.stream()
  161.                                        .filter(s -> !s.startsWith(ROLE_STYLE_PREFIX))
  162.                                        .collect(toList());
  163.         // FIXME: shouldn't reset them? In case of cell reuse, they get accumulated
  164.         styles.addAll(roles.getMany(_Styleable_)
  165.                            .stream()
  166.                            .flatMap(styleable -> styleable.getStyles().stream())
  167.                            .map(s -> ROLE_STYLE_PREFIX + s)
  168.                            .collect(toList()));
  169.         styleClasses.setAll(styles);
  170.       }

  171.     /***********************************************************************************************************************************************************
  172.      * 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
  173.      * untestable.
  174.      * @param     roles           the role bag
  175.      * @return                    the list of {@link MenuItem}s
  176.      **********************************************************************************************************************************************************/
  177.     @Nonnull
  178.     @VisibleForTesting public List<MenuItem> createMenuItems (@Nonnull final RoleCollector roles)
  179.       {
  180.         return roles.getMany(_UserActionProvider_).stream()
  181.                     .flatMap(uap -> uap.getActions().stream())
  182.                     .map(this::createMenuItem)
  183.                     .collect(toList());
  184.       }

  185.     /***********************************************************************************************************************************************************
  186.      * Creates a {@link MenuItem} bound to the given action.
  187.      * @param     action          the action
  188.      * @return                    the bound {@code MenuItem}
  189.      **********************************************************************************************************************************************************/
  190.     @Nonnull
  191.     private MenuItem createMenuItem (@Nonnull final UserAction action)
  192.       {
  193.         final var menuItem = new MenuItem(action.as(_Displayable_).getDisplayName());
  194.         menuItem.setOnAction(new EventHandlerUserActionAdapter(executor, action));
  195.         return menuItem;
  196.       }
  197.   }