Skip to contentMethod: DefaultJavaFXPanelGroupControl(BeanFactory)
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:
28: import java.lang.reflect.InvocationTargetException;
29: import jakarta.annotation.Nonnull;
30: import jakarta.annotation.PostConstruct;
31: import jakarta.annotation.PreDestroy;
32: import java.util.Arrays;
33: import java.util.HashMap;
34: import java.util.List;
35: import java.util.Map;
36: import java.util.Optional;
37: import javafx.scene.Node;
38: import javafx.scene.control.Accordion;
39: import javafx.scene.control.TitledPane;
40: import javafx.scene.layout.AnchorPane;
41: import javafx.scene.layout.StackPane;
42: import org.springframework.beans.BeansException;
43: import org.springframework.beans.factory.BeanFactory;
44: import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
45: import it.tidalwave.ui.core.PanelGroupControl;
46: import it.tidalwave.ui.core.PanelGroupProvider;
47: import it.tidalwave.ui.core.spi.PanelGroupControlSupport;
48: import it.tidalwave.ui.core.message.PanelShowRequest;
49: import it.tidalwave.ui.javafx.JavaFXPanelGroupControl;
50: import it.tidalwave.ui.javafx.NodeAndDelegate;
51: import it.tidalwave.util.Pair;
52: import it.tidalwave.util.annotation.VisibleForTesting;
53: import it.tidalwave.messagebus.MessageBus;
54: import lombok.extern.slf4j.Slf4j;
55: import static it.tidalwave.ui.core.PanelGroupControl.Options.*;
56: import static it.tidalwave.ui.javafx.impl.DefaultJavaFXBinder.enforceFxApplicationThread;
57: import static it.tidalwave.ui.javafx.spi.AbstractJavaFXSpringApplication.APPLICATION_MESSAGE_BUS_BEAN_NAME;
58: import static java.util.Comparator.*;
59: import static java.util.stream.Collectors.*;
60: import static it.tidalwave.util.CollectionUtils.concatAll;
61:
62: /***************************************************************************************************************************************************************
63: *
64: * The JavaFX implementation of {@link PanelGroupControl}.
65: *
66: * @since 2.0-ALPHA-3
67: * @author Fabrizio Giudici
68: *
69: **************************************************************************************************************************************************************/
70: @Slf4j @SuppressFBWarnings("MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR")
71: public class DefaultJavaFXPanelGroupControl extends PanelGroupControlSupport<AnchorPane, Node> implements JavaFXPanelGroupControl
72: {
73: private static final Runnable DO_NOTHING = () -> {};
74:
75: /** A map associating to each managed {@code Node} a callback that makes it visible. */
76: private final Map<Node, Runnable> expanderByNode = new HashMap<>();
77:
78: /** The message bus, if present on the system. */
79: @Nonnull
80: private final Optional<MessageBus> messageBus;
81:
82: /** The listener to the message bus. */
83: private final MessageBus.Listener<PanelShowRequest> messageListener = this::onShowRequest;
84:
85: /***********************************************************************************************************************************************************
86: * This class doesn't rely on the @SimpleSubscriber/@ListensTo annotations to avoid having a dependency on the MessageBus runtime, so it dynamically
87: * queries a {@link BeanFactory}.
88: **********************************************************************************************************************************************************/
89: public DefaultJavaFXPanelGroupControl (@Nonnull final BeanFactory beanFactory)
90: {
91: MessageBus messageBus = null;
92:
93: try
94: {
95: messageBus = beanFactory.getBean(APPLICATION_MESSAGE_BUS_BEAN_NAME, MessageBus.class);
96: }
97: catch (BeansException e)
98: {
99: log.warn("No message bus");
100: }
101:
102: this.messageBus = Optional.ofNullable(messageBus);
103: }
104:
105: /***********************************************************************************************************************************************************
106: * {@inheritDoc}
107: **********************************************************************************************************************************************************/
108: @Override
109: public void show (@Nonnull final Object requestor)
110: {
111: expanderByNode.getOrDefault(findNode(requestor), DO_NOTHING).run();
112: }
113:
114: /***********************************************************************************************************************************************************
115: *
116: **********************************************************************************************************************************************************/
117: @PostConstruct
118: @VisibleForTesting void initialize()
119: {
120: messageBus.ifPresent(mb -> mb.subscribe(PanelShowRequest.class, messageListener));
121: }
122:
123: /***********************************************************************************************************************************************************
124: *
125: **********************************************************************************************************************************************************/
126: @PreDestroy
127: @VisibleForTesting void destroy()
128: {
129: messageBus.ifPresent(mb -> mb.unsubscribe(messageListener));
130: }
131:
132: /***********************************************************************************************************************************************************
133: * {@inheritDoc}
134: **********************************************************************************************************************************************************/
135: @Override
136: protected void assemble (@Nonnull final Group group,
137: @Nonnull final List<? extends PanelGroupProvider<Node>> panelProviders,
138: @Nonnull final AnchorPane topContainer,
139: @Nonnull final Map<Group, List<Options>> groupOptions,
140: @Nonnull final List<Options> globalOptions)
141: {
142: enforceFxApplicationThread();
143: final var options = concatAll(globalOptions, groupOptions.getOrDefault(group, List.of()));
144:
145: if (!options.contains(ALWAYS_WRAP) && panelProviders.size() == 1)
146: {
147: final var node = panelProviders.get(0).getComponent();
148: log.info("{}: options: {} --- using no wrapper for {}", group, options, node);
149: put(topContainer, node);
150: }
151: else if (options.contains(USE_ACCORDION))
152: {
153: final var accordion = new Accordion();
154: put(topContainer, accordion);
155: final var titledPanes = panelProviders.stream()
156: .sorted(comparing(PanelGroupProvider::getLabel))
157: .map(p -> Pair.of(p.getComponent(), createTitledPane(p, globalOptions)))
158: .collect(toList()); // pair of (Node, TitledPane)
159: final var nodes = titledPanes.stream().map(Pair::getB).collect(toList());
160: log.info("{}: options: {} --- using Accordion for {}", group, options, nodes);
161:
162: if (nodes.isEmpty())
163: {
164: accordion.setVisible(false);
165: }
166: else
167: {
168: accordion.getPanes().setAll(nodes);
169: titledPanes.forEach(p -> expanderByNode.put(p.a, () -> accordion.setExpandedPane(p.b)));
170: }
171: }
172: else
173: {
174: final var stackPane = new StackPane();
175: put(topContainer, stackPane);
176: final var nodes = panelProviders.stream()
177: .sorted(comparing(PanelGroupProvider::getLabel))
178: .map(PanelGroupProvider::getComponent)
179: .collect(toList());
180: log.info("{}: options: {} --- using StackPane for {}", group, options, nodes);
181: nodes.forEach(n -> n.setVisible(false));
182: nodes.forEach(n -> expanderByNode.put(n, () -> n.setVisible(true)));
183:
184: if (nodes.isEmpty())
185: {
186: stackPane.setVisible(false);
187: }
188: else
189: {
190: stackPane.getChildren().setAll(nodes);
191: }
192: }
193: }
194:
195: /***********************************************************************************************************************************************************
196: *
197: **********************************************************************************************************************************************************/
198: @VisibleForTesting final void onShowRequest (@Nonnull final PanelShowRequest panelShowRequest)
199: {
200: log.info("onShowRequest({})", panelShowRequest);
201: show(panelShowRequest.getRequestor());
202: messageBus.ifPresent(mb -> mb.publish(panelShowRequest.createResponse()));
203: }
204:
205: /***********************************************************************************************************************************************************
206: * {@return a {@link Node} extracted from a presentation}. If the presentation is not a {@code Node} itself, it is searched for a method returning
207: * {@link NodeAndDelegate}.
208: * @param presentation the presentation
209: **********************************************************************************************************************************************************/
210: @Nonnull
211: private static Node findNode (@Nonnull final Object presentation)
212: {
213: if (presentation instanceof Node)
214: {
215: return (Node)presentation;
216: }
217:
218: final var method = Arrays.stream(presentation.getClass().getDeclaredMethods())
219: .filter(m -> m.getReturnType().equals(NodeAndDelegate.class))
220: .findFirst()
221: .orElseThrow(() -> new RuntimeException("Can't find method returning NodeAndDelegate in " + presentation));
222: try
223: {
224: final var nad = (NodeAndDelegate<?>)method.invoke(presentation);
225: return nad.getNode();
226: }
227: catch (IllegalAccessException | InvocationTargetException e)
228: {
229: final var message = "Couldn't extract a Node out of " + presentation;
230: log.error(message, e);
231: throw new RuntimeException(message, e);
232: }
233: }
234:
235: /***********************************************************************************************************************************************************
236: * {@return a {@link TitledPane} wrapping the panel provided by the given provider.
237: * @param panelGroupProvider the provider
238: * @param options the options
239: **********************************************************************************************************************************************************/
240: @Nonnull
241: private TitledPane createTitledPane (@Nonnull final PanelGroupProvider<? extends Node> panelGroupProvider, @Nonnull final List<Options> options)
242: {
243: final var titledPane = new TitledPane();
244: titledPane.setText(panelGroupProvider.getLabel());
245: titledPane.animatedProperty().set(!options.contains(DISABLE_ACCORDION_ANIMATION));
246: titledPane.setContent(panelGroupProvider.getComponent());
247: return titledPane;
248: }
249:
250: /***********************************************************************************************************************************************************
251: * Puts the given {@link Node} inside an {@link AnchorPane}.
252: * @param anchorPane the {@code AnchorPane}
253: * @param node the {@code Node}
254: **********************************************************************************************************************************************************/
255: private static void put (@Nonnull final AnchorPane anchorPane, @Nonnull final Node node)
256: {
257: AnchorPane.setLeftAnchor(node, 0.0); // TODO: maybe useless?
258: AnchorPane.setRightAnchor(node, 0.0);
259: AnchorPane.setTopAnchor(node, 0.0);
260: AnchorPane.setBottomAnchor(node, 0.0);
261: anchorPane.getChildren().setAll(node);
262: }
263: }