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