DefaultJavaFXPanelGroupControl.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;

  27. import java.lang.reflect.InvocationTargetException;
  28. import jakarta.annotation.Nonnull;
  29. import java.util.Arrays;
  30. import java.util.IdentityHashMap;
  31. import java.util.List;
  32. import java.util.Map;
  33. import java.util.Optional;
  34. import javafx.scene.Node;
  35. import javafx.scene.control.Accordion;
  36. import javafx.scene.control.TitledPane;
  37. import javafx.scene.layout.AnchorPane;
  38. import javafx.scene.layout.StackPane;
  39. import org.springframework.beans.factory.BeanFactory;
  40. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  41. import it.tidalwave.ui.core.PanelGroupControl;
  42. import it.tidalwave.ui.core.PanelGroupProvider;
  43. import it.tidalwave.ui.core.message.PanelHiddenNotification;
  44. import it.tidalwave.ui.core.message.PanelShownNotification;
  45. import it.tidalwave.ui.core.spi.PanelGroupControlSupport;
  46. import it.tidalwave.ui.core.message.PanelShowRequest;
  47. import it.tidalwave.ui.javafx.JavaFXPanelGroupControl;
  48. import it.tidalwave.ui.javafx.NodeAndDelegate;
  49. import it.tidalwave.util.Pair;
  50. import lombok.extern.slf4j.Slf4j;
  51. import static it.tidalwave.ui.core.PanelGroupControl.Options.*;
  52. import static it.tidalwave.ui.javafx.impl.DefaultJavaFXBinder.enforceFxApplicationThread;
  53. import static it.tidalwave.ui.javafx.spi.AbstractJavaFXSpringApplication.APPLICATION_MESSAGE_BUS_BEAN_NAME;
  54. import static java.util.Comparator.*;
  55. import static java.util.stream.Collectors.*;
  56. import static it.tidalwave.util.CollectionUtils.concatAll;

  57. /***************************************************************************************************************************************************************
  58.  *
  59.  * The JavaFX implementation of {@link PanelGroupControl}.
  60.  *
  61.  * @since       2.0-ALPHA-3
  62.  * @author      Fabrizio Giudici
  63.  *
  64.  **************************************************************************************************************************************************************/
  65. @Slf4j @SuppressFBWarnings("MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR")
  66. public class DefaultJavaFXPanelGroupControl extends PanelGroupControlSupport<AnchorPane, Node> implements JavaFXPanelGroupControl
  67.   {
  68.     private static final Runnable DO_NOTHING = () -> {};

  69.     /** A map associating to each managed {@code Node} a callback that makes it visible. */
  70.     private final Map<Node, Runnable> expanderByNode = new IdentityHashMap<>();

  71.     /* A map associating to each managed {@Code Node} the group it belongs to. */
  72.     private final Map<Node, Group> groupByNode = new IdentityHashMap<>();

  73.     /* A map associating to each managed {@Code Node} its provider. */
  74.     private final Map<Node, PanelGroupProvider<Node>> providerByNode = new IdentityHashMap<>();

  75.     /***********************************************************************************************************************************************************
  76.      *
  77.      **********************************************************************************************************************************************************/
  78.     public DefaultJavaFXPanelGroupControl (@Nonnull final BeanFactory beanFactory)
  79.       {
  80.         super(beanFactory, APPLICATION_MESSAGE_BUS_BEAN_NAME);
  81.       }

  82.     /***********************************************************************************************************************************************************
  83.      * {@inheritDoc}
  84.      **********************************************************************************************************************************************************/
  85.     @Override
  86.     public void show (@Nonnull final Object requestor)
  87.       {
  88.         enforceFxApplicationThread();
  89.         expanderByNode.getOrDefault(findNode(requestor), DO_NOTHING).run();
  90.       }

  91.     /***********************************************************************************************************************************************************
  92.      * {@inheritDoc}
  93.      **********************************************************************************************************************************************************/
  94.     @Override
  95.     protected void assemble (@Nonnull final Group group,
  96.                              @Nonnull final List<? extends PanelGroupProvider<Node>> panelProviders,
  97.                              @Nonnull final AnchorPane topContainer,
  98.                              @Nonnull final Map<Group, List<Options>> groupOptions,
  99.                              @Nonnull final List<Options> globalOptions)
  100.       {
  101.         enforceFxApplicationThread();
  102.         final var options = concatAll(globalOptions, groupOptions.getOrDefault(group, List.of()));
  103.         panelProviders.forEach(p -> providerByNode.put(p.getComponent(), p));

  104.         if (!options.contains(ALWAYS_WRAP) && panelProviders.size() == 1)
  105.           {
  106.             addDirectly(group, panelProviders, topContainer, options);
  107.           }
  108.         else if (options.contains(USE_ACCORDION))
  109.           {
  110.             addInAccordion(group, panelProviders, topContainer, options);
  111.           }
  112.         else
  113.           {
  114.             addInStackPane(group, panelProviders, topContainer, options);
  115.           }
  116.       }

  117.     /***********************************************************************************************************************************************************
  118.      *
  119.      **********************************************************************************************************************************************************/
  120.     @Override
  121.     protected void onShowRequest (@Nonnull final PanelShowRequest panelShowRequest)
  122.       {
  123.         log.info("onShowRequest({})", panelShowRequest);
  124.         expanderByNode.getOrDefault(findNode(panelShowRequest.getRequestor()), DO_NOTHING).run();
  125.       }

  126.     /***********************************************************************************************************************************************************
  127.      *
  128.      **********************************************************************************************************************************************************/
  129.     private void addDirectly (@Nonnull final Group group,
  130.                               @Nonnull final List<? extends PanelGroupProvider<Node>> panelProviders,
  131.                               @Nonnull final AnchorPane topContainer,
  132.                               @Nonnull final List<Options> options)
  133.       {
  134.         final var node = put(topContainer, panelProviders.get(0).getComponent());
  135.         groupByNode.put(node, group);
  136.         log.info("{}: options: {} --- no wrapper for {}", group, options, node);
  137.       }

  138.     /***********************************************************************************************************************************************************
  139.      *
  140.      **********************************************************************************************************************************************************/
  141.     private void addInAccordion (@Nonnull final Group group,
  142.                                  @Nonnull final List<? extends PanelGroupProvider<Node>> panelProviders,
  143.                                  @Nonnull final AnchorPane topContainer,
  144.                                  @Nonnull final List<Options> options)
  145.       {
  146.         final var accordion = put(topContainer, new Accordion());
  147.         final var nodesAndTitlePanes = panelProviders.stream()
  148.                                                      .sorted(comparing(PanelGroupProvider::getLabel))
  149.                                                      .map(p -> Pair.of(p.getComponent(), createTitledPane(p, options)))
  150.                                                      .collect(toList()); // pair of (Node, TitledPane)
  151.         final var titlePanes = nodesAndTitlePanes.stream().map(Pair::getB).collect(toList());
  152.         accordion.setVisible(!titlePanes.isEmpty());
  153.         accordion.getPanes().setAll(titlePanes);
  154.         nodesAndTitlePanes.forEach(p ->
  155.           {
  156.             groupByNode.put(p.a, group);
  157.             expanderByNode.put(p.a, () -> accordion.setExpandedPane(p.b));
  158.           });
  159.         // Needs a listener because the change might be originated by the user
  160.         accordion.expandedPaneProperty().addListener((observable, collapsed, expanded) ->
  161.                 fireUpdateMessages(Optional.ofNullable(collapsed).map(TitledPane::getContent),
  162.                                    Optional.ofNullable(expanded).map(TitledPane::getContent)));
  163.         log.info("{}: options: {} --- Accordion for {}", group, options, titlePanes);
  164.       }

  165.     /***********************************************************************************************************************************************************
  166.      *
  167.      **********************************************************************************************************************************************************/
  168.     private void addInStackPane (@Nonnull final Group group,
  169.                                  @Nonnull final List<? extends PanelGroupProvider<Node>> panelProviders,
  170.                                  @Nonnull final AnchorPane topContainer,
  171.                                  @Nonnull final List<Options> options)
  172.       {
  173.         final var stackPane = put(topContainer, new StackPane());
  174.         final var nodes = panelProviders.stream()
  175.                                         .sorted(comparing(PanelGroupProvider::getLabel))
  176.                                         .map(PanelGroupProvider::getComponent)
  177.                                         .collect(toList());
  178.         nodes.forEach(n ->
  179.           {
  180.             n.setVisible(false);
  181.             groupByNode.put(n, group);
  182.             expanderByNode.put(n, () -> nodes.forEach(n2 -> n2.setVisible(n2 == n)));
  183.             n.visibleProperty().addListener((observable, oldVisible, visible) ->
  184.                     fireUpdateMessages(visible ? Optional.empty() : Optional.of(n),
  185.                                        visible ? Optional.of(n) : Optional.empty()));
  186.           });

  187.         stackPane.setVisible(!nodes.isEmpty());
  188.         stackPane.getChildren().setAll(nodes);
  189.         log.info("{}: options: {} --- StackPane for {}", group, options, nodes);
  190.       }

  191.     /***********************************************************************************************************************************************************
  192.      * Fires update messages after a change.
  193.      * @param     hiddenNode         the old node
  194.      * @param     shownNode         the new node
  195.      **********************************************************************************************************************************************************/
  196.     private void fireUpdateMessages (@Nonnull final Optional<Node> hiddenNode, @Nonnull final Optional<Node> shownNode)
  197.       {
  198.         hiddenNode.ifPresent(n -> publish(new PanelHiddenNotification(providerByNode.get(n).getPresentation(), groupByNode.get(n))));
  199.         shownNode.ifPresent(n -> publish(new PanelShownNotification(providerByNode.get(n).getPresentation(), groupByNode.get(n))));
  200.       }

  201.     /***********************************************************************************************************************************************************
  202.      * {@return a {@link Node} extracted from a presentation}. If the presentation is not a {@code Node} itself, it is searched for a method returning
  203.      * {@link NodeAndDelegate}.
  204.      * @param     presentation      the presentation
  205.      **********************************************************************************************************************************************************/
  206.     @Nonnull
  207.     private static Node findNode (@Nonnull final Object presentation)
  208.       {
  209.         if (presentation instanceof Node)
  210.           {
  211.             return (Node)presentation;
  212.           }

  213.         final var method = Arrays.stream(presentation.getClass().getDeclaredMethods())
  214.                                  .filter(m -> m.getReturnType().equals(NodeAndDelegate.class))
  215.                                  .findFirst()
  216.                                  .orElseThrow(() -> new RuntimeException("Can't find method returning NodeAndDelegate in " + presentation));
  217.         try
  218.           {
  219.             final var nad = (NodeAndDelegate<?>)method.invoke(presentation);
  220.             return nad.getNode();
  221.           }
  222.         catch (IllegalAccessException | InvocationTargetException e)
  223.           {
  224.             final var message = "Couldn't extract a Node out of " + presentation;
  225.             log.error(message, e);
  226.             throw new RuntimeException(message, e);
  227.           }
  228.       }

  229.     /***********************************************************************************************************************************************************
  230.      * {@return a {@link TitledPane} wrapping the panel provided by the given provider.
  231.      * @param   panelGroupProvider   the provider
  232.      * @param   options         the options
  233.      **********************************************************************************************************************************************************/
  234.     @Nonnull
  235.     private TitledPane createTitledPane (@Nonnull final PanelGroupProvider<? extends Node> panelGroupProvider, @Nonnull final List<Options> options)
  236.       {
  237.         final var titledPane = new TitledPane();
  238.         titledPane.setText(panelGroupProvider.getLabel());
  239.         titledPane.animatedProperty().set(!options.contains(DISABLE_ACCORDION_ANIMATION));
  240.         titledPane.setContent(panelGroupProvider.getComponent());
  241.         return titledPane;
  242.       }

  243.     /***********************************************************************************************************************************************************
  244.      * Puts the given {@link Node} inside an {@link AnchorPane}.
  245.      * @param   anchorPane    the {@code AnchorPane}
  246.      * @param   node          the {@code Node}
  247.      **********************************************************************************************************************************************************/
  248.     @Nonnull
  249.     private static <T extends Node> T put (@Nonnull final AnchorPane anchorPane, @Nonnull final T node)
  250.       {
  251.         AnchorPane.setLeftAnchor(node, 0.0); // TODO: maybe useless?
  252.         AnchorPane.setRightAnchor(node, 0.0);
  253.         AnchorPane.setTopAnchor(node, 0.0);
  254.         AnchorPane.setBottomAnchor(node, 0.0);
  255.         anchorPane.getChildren().setAll(node);
  256.         return node;
  257.       }
  258.   }