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 jakarta.annotation.PostConstruct;
  30. import jakarta.annotation.PreDestroy;
  31. import java.util.Arrays;
  32. import java.util.HashMap;
  33. import java.util.List;
  34. import java.util.Map;
  35. import java.util.Optional;
  36. import javafx.scene.Node;
  37. import javafx.scene.control.Accordion;
  38. import javafx.scene.control.TitledPane;
  39. import javafx.scene.layout.AnchorPane;
  40. import javafx.scene.layout.StackPane;
  41. import org.springframework.beans.BeansException;
  42. import org.springframework.beans.factory.BeanFactory;
  43. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  44. import it.tidalwave.ui.core.PanelGroupControl;
  45. import it.tidalwave.ui.core.PanelGroupProvider;
  46. import it.tidalwave.ui.core.spi.PanelGroupControlSupport;
  47. import it.tidalwave.ui.core.message.PanelShowRequest;
  48. import it.tidalwave.ui.javafx.JavaFXPanelGroupControl;
  49. import it.tidalwave.ui.javafx.NodeAndDelegate;
  50. import it.tidalwave.util.Pair;
  51. import it.tidalwave.util.annotation.VisibleForTesting;
  52. import it.tidalwave.messagebus.MessageBus;
  53. import lombok.extern.slf4j.Slf4j;
  54. import static it.tidalwave.ui.core.PanelGroupControl.Options.*;
  55. import static it.tidalwave.ui.javafx.impl.DefaultJavaFXBinder.enforceFxApplicationThread;
  56. import static it.tidalwave.ui.javafx.spi.AbstractJavaFXSpringApplication.APPLICATION_MESSAGE_BUS_BEAN_NAME;
  57. import static java.util.Comparator.*;
  58. import static java.util.stream.Collectors.*;
  59. import static it.tidalwave.util.CollectionUtils.concatAll;

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

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

  74.     /** The message bus, if present on the system. */
  75.     @Nonnull
  76.     private final Optional<MessageBus> messageBus;

  77.     /** The listener to the message bus. */
  78.     private final MessageBus.Listener<PanelShowRequest> messageListener = this::onShowRequest;

  79.     /***********************************************************************************************************************************************************
  80.      * This class doesn't rely on the @SimpleSubscriber/@ListensTo annotations to avoid having a dependency on the MessageBus runtime, so it dynamically
  81.      * queries a {@link BeanFactory}.
  82.      **********************************************************************************************************************************************************/
  83.     public DefaultJavaFXPanelGroupControl (@Nonnull final BeanFactory beanFactory)
  84.       {
  85.         MessageBus messageBus = null;

  86.         try
  87.           {
  88.             messageBus = beanFactory.getBean(APPLICATION_MESSAGE_BUS_BEAN_NAME, MessageBus.class);
  89.           }
  90.         catch (BeansException e)
  91.           {
  92.             log.warn("No message bus");
  93.           }

  94.         this.messageBus = Optional.ofNullable(messageBus);
  95.       }

  96.     /***********************************************************************************************************************************************************
  97.      * {@inheritDoc}
  98.      **********************************************************************************************************************************************************/
  99.     @Override
  100.     public void show (@Nonnull final Object requestor)
  101.       {
  102.         expanderByNode.getOrDefault(findNode(requestor), DO_NOTHING).run();
  103.       }

  104.     /***********************************************************************************************************************************************************
  105.      *
  106.      **********************************************************************************************************************************************************/
  107.     @PostConstruct
  108.     @VisibleForTesting void initialize()
  109.       {
  110.         messageBus.ifPresent(mb -> mb.subscribe(PanelShowRequest.class, messageListener));
  111.       }

  112.     /***********************************************************************************************************************************************************
  113.      *
  114.      **********************************************************************************************************************************************************/
  115.     @PreDestroy
  116.     @VisibleForTesting void destroy()
  117.       {
  118.         messageBus.ifPresent(mb -> mb.unsubscribe(messageListener));
  119.       }

  120.     /***********************************************************************************************************************************************************
  121.      * {@inheritDoc}
  122.      **********************************************************************************************************************************************************/
  123.     @Override
  124.     protected void assemble (@Nonnull final Group group,
  125.                              @Nonnull final List<? extends PanelGroupProvider<Node>> panelProviders,
  126.                              @Nonnull final AnchorPane topContainer,
  127.                              @Nonnull final Map<Group, List<Options>> groupOptions,
  128.                              @Nonnull final List<Options> globalOptions)
  129.       {
  130.         enforceFxApplicationThread();
  131.         final var options = concatAll(globalOptions, groupOptions.getOrDefault(group, List.of()));

  132.         if (!options.contains(ALWAYS_WRAP) && panelProviders.size() == 1)
  133.           {
  134.             final var node = panelProviders.get(0).getComponent();
  135.             log.info("{}: options: {} --- using no wrapper for {}", group, options, node);
  136.             put(topContainer, node);
  137.           }
  138.         else if (options.contains(USE_ACCORDION))
  139.           {
  140.             final var accordion = new Accordion();
  141.             put(topContainer, accordion);
  142.             final var titledPanes = panelProviders.stream()
  143.                                                   .sorted(comparing(PanelGroupProvider::getLabel))
  144.                                                   .map(p -> Pair.of(p.getComponent(), createTitledPane(p, globalOptions)))
  145.                                                   .collect(toList()); // pair of (Node, TitledPane)
  146.             final var nodes = titledPanes.stream().map(Pair::getB).collect(toList());
  147.             log.info("{}: options: {} ---  using Accordion for {}", group, options, nodes);

  148.             if (nodes.isEmpty())
  149.               {
  150.                 accordion.setVisible(false);
  151.               }
  152.             else
  153.               {
  154.                 accordion.getPanes().setAll(nodes);
  155.                 titledPanes.forEach(p -> expanderByNode.put(p.a, () -> accordion.setExpandedPane(p.b)));
  156.               }
  157.           }
  158.         else
  159.           {
  160.             final var stackPane = new StackPane();
  161.             put(topContainer, stackPane);
  162.             final var nodes = panelProviders.stream()
  163.                                             .sorted(comparing(PanelGroupProvider::getLabel))
  164.                                             .map(PanelGroupProvider::getComponent)
  165.                                             .collect(toList());
  166.             log.info("{}: options: {} ---  using StackPane for {}", group, options, nodes);
  167.             nodes.forEach(n -> n.setVisible(false));
  168.             nodes.forEach(n -> expanderByNode.put(n, () -> n.setVisible(true)));

  169.             if (nodes.isEmpty())
  170.               {
  171.                 stackPane.setVisible(false);
  172.               }
  173.             else
  174.               {
  175.                 stackPane.getChildren().setAll(nodes);
  176.               }
  177.           }
  178.       }

  179.     /***********************************************************************************************************************************************************
  180.      *
  181.      **********************************************************************************************************************************************************/
  182.     @VisibleForTesting final void onShowRequest (@Nonnull final PanelShowRequest panelShowRequest)
  183.       {
  184.         log.info("onShowRequest({})", panelShowRequest);
  185.         show(panelShowRequest.getRequestor());
  186.         messageBus.ifPresent(mb -> mb.publish(panelShowRequest.createResponse()));
  187.       }

  188.     /***********************************************************************************************************************************************************
  189.      * {@return a {@link Node} extracted from a presentation}. If the presentation is not a {@code Node} itself, it is searched for a method returning
  190.      * {@link NodeAndDelegate}.
  191.      * @param     presentation      the presentation
  192.      **********************************************************************************************************************************************************/
  193.     @Nonnull
  194.     private static Node findNode (@Nonnull final Object presentation)
  195.       {
  196.         if (presentation instanceof Node)
  197.           {
  198.             return (Node)presentation;
  199.           }

  200.         final var method = Arrays.stream(presentation.getClass().getDeclaredMethods())
  201.                                  .filter(m -> m.getReturnType().equals(NodeAndDelegate.class))
  202.                                  .findFirst()
  203.                                  .orElseThrow(() -> new RuntimeException("Can't find method returning NodeAndDelegate in " + presentation));
  204.         try
  205.           {
  206.             final var nad = (NodeAndDelegate<?>)method.invoke(presentation);
  207.             return nad.getNode();
  208.           }
  209.         catch (IllegalAccessException | InvocationTargetException e)
  210.           {
  211.             final var message = "Couldn't extract a Node out of " + presentation;
  212.             log.error(message, e);
  213.             throw new RuntimeException(message, e);
  214.           }
  215.       }

  216.     /***********************************************************************************************************************************************************
  217.      * {@return a {@link TitledPane} wrapping the panel provided by the given provider.
  218.      * @param   panelGroupProvider   the provider
  219.      * @param   options         the options
  220.      **********************************************************************************************************************************************************/
  221.     @Nonnull
  222.     private TitledPane createTitledPane (@Nonnull final PanelGroupProvider<? extends Node> panelGroupProvider, @Nonnull final List<Options> options)
  223.       {
  224.         final var titledPane = new TitledPane();
  225.         titledPane.setText(panelGroupProvider.getLabel());
  226.         titledPane.animatedProperty().set(!options.contains(DISABLE_ACCORDION_ANIMATION));
  227.         titledPane.setContent(panelGroupProvider.getComponent());
  228.         return titledPane;
  229.       }

  230.     /***********************************************************************************************************************************************************
  231.      * Puts the given {@link Node} inside an {@link AnchorPane}.
  232.      * @param   anchorPane    the {@code AnchorPane}
  233.      * @param   node          the {@code Node}
  234.      **********************************************************************************************************************************************************/
  235.     private static void put (@Nonnull final AnchorPane anchorPane, @Nonnull final Node node)
  236.       {
  237.         AnchorPane.setLeftAnchor(node, 0.0); // TODO: maybe useless?
  238.         AnchorPane.setRightAnchor(node, 0.0);
  239.         AnchorPane.setTopAnchor(node, 0.0);
  240.         AnchorPane.setBottomAnchor(node, 0.0);
  241.         anchorPane.getChildren().setAll(node);
  242.       }
  243.   }