Skip to content

Method: static {...}

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