NamedCardPane.java
001 /*
002  * Copyright 2008-2017 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *     http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package griffon.javafx.scene.layout;
017 
018 import javafx.application.Platform;
019 import javafx.beans.property.ObjectProperty;
020 import javafx.beans.property.ReadOnlyObjectProperty;
021 import javafx.beans.property.ReadOnlyStringProperty;
022 import javafx.beans.property.SimpleObjectProperty;
023 import javafx.beans.property.SimpleStringProperty;
024 import javafx.beans.property.StringProperty;
025 import javafx.collections.ListChangeListener;
026 import javafx.scene.Node;
027 import javafx.scene.layout.Region;
028 import javafx.scene.layout.StackPane;
029 
030 import javax.annotation.Nonnull;
031 import javax.annotation.Nullable;
032 import java.util.Map;
033 import java.util.concurrent.ConcurrentHashMap;
034 import java.util.concurrent.atomic.AtomicBoolean;
035 
036 import static griffon.util.GriffonClassUtils.requireState;
037 import static griffon.util.GriffonNameUtils.isBlank;
038 import static griffon.util.GriffonNameUtils.requireNonBlank;
039 import static java.util.Objects.requireNonNull;
040 
041 /**
042  @author Andres Almiray
043  @since 2.11.0
044  */
045 public class NamedCardPane extends StackPane {
046     private static final String ERROR_NODE_NULL = "Argument 'node' must not be null";
047     private static final String ERROR_ID_NULL = "Argument 'id' must not be null";
048 
049     private final Map<String, Node> nodes = new ConcurrentHashMap<>();
050     private final StringProperty selectedNodeId = new SimpleStringProperty(this, "selectedNodeId");
051     private final ObjectProperty<Node> selectedNode = new SimpleObjectProperty<>(this, "selectedNode");
052     private final AtomicBoolean adjusting = new AtomicBoolean(false);
053 
054     public NamedCardPane() {
055         getStyleClass().add("named-cardpane");
056 
057         getChildren().addListener((ListChangeListener<Node>c -> {
058             if (adjusting.get()) {
059                 return;
060             }
061 
062             while (c.next()) {
063                 if (c.wasAdded()) {
064                     Node lastAddedNode = null;
065                     for (Node node : c.getAddedSubList()) {
066                         String id = node.getId();
067                         requireNonBlank(id, ERROR_ID_NULL);
068                         if (!nodes.containsKey(id)) {
069                             nodes.put(id, node);
070                             lastAddedNode = node;
071                         }
072                     }
073 
074                     if (lastAddedNode != null) {
075                         String nextId = lastAddedNode.getId();
076                         doShow(nextId);
077                     }
078 
079                 else if (c.wasRemoved()) {
080                     String selectedId = getSelectedNodeId();
081                     boolean selectionChanged = false;
082                     for (Node node : c.getRemoved()) {
083                         String id = node.getId();
084                         requireNonBlank(id, ERROR_ID_NULL);
085                         if (!nodes.containsKey(id)) {
086                             continue;
087                         }
088 
089                         nodes.remove(id);
090 
091                         if (id.equals(selectedId)) {
092                             selectionChanged = true;
093                         }
094                     }
095 
096                     if (selectionChanged) {
097                         final String nextId = nodes.size() ? nodes.keySet().iterator().next() null;
098                         doShow(nextId);
099                     }
100                 }
101             }
102         });
103 
104         widthProperty().addListener((observable, oldValue, newValue-> updateBoundsInChildren());
105         heightProperty().addListener((observable, oldValue, newValue-> updateBoundsInChildren());
106     }
107 
108     protected void updateBoundsInChildren() {
109         nodes.values().forEach(this::updateChildBounds);
110         layout();
111     }
112 
113     protected void updateChildBounds(Node node) {
114         if (node instanceof Region) {
115             Region child = (Regionnode;
116             child.setPrefWidth(getWidth());
117             child.setPrefHeight(getHeight());
118         }
119     }
120 
121     @Nonnull
122     public ReadOnlyStringProperty selectedNodeIdProperty() {
123         return selectedNodeId;
124     }
125 
126     @Nonnull
127     public ReadOnlyObjectProperty<Node> selectedNodeProperty() {
128         return selectedNode;
129     }
130 
131     @Nullable
132     public String getSelectedNodeId() {
133         return selectedNodeId.get();
134     }
135 
136     @Nullable
137     public Node getSelectedNode() {
138         return selectedNode.get();
139     }
140 
141     public boolean isEmpty() {
142         return nodes.isEmpty();
143     }
144 
145     public int size() {
146         return nodes.size();
147     }
148 
149     public void clear() {
150         nodes.clear();
151         getChildren().clear();
152         selectedNodeId.set(null);
153         selectedNode.set(null);
154     }
155 
156     public void add(@Nonnull final String id, @Nonnull final Node node) {
157         requireNonNull(node, ERROR_NODE_NULL);
158         requireNonBlank(id, ERROR_ID_NULL);
159 
160         adjusting.set(true);
161         nodes.put(id, node);
162         adjusting.set(false);
163 
164         show(id);
165     }
166 
167     public void remove(@Nonnull String id) {
168         requireNonBlank(id, ERROR_ID_NULL);
169         if (!nodes.containsKey(id)) {
170             return;
171         }
172 
173         String selectedId = getSelectedNodeId();
174         adjusting.set(true);
175         nodes.remove(id);
176         adjusting.set(false);
177 
178         if (id.equals(selectedId)) {
179             String nextId = null;
180             if (nodes.size() 0) {
181                 nextId = nodes.keySet().iterator().next();
182             }
183             doShow(nextId);
184         }
185     }
186 
187     public void show(@Nonnull String id) {
188         requireNonBlank(id, ERROR_ID_NULL);
189         requireState(nodes.containsKey(id)"No content associated with id '" + id + "'");
190         doShow(id);
191     }
192 
193     protected void doShow(@Nullable String id) {
194         Platform.runLater(() -> {
195             if (isBlank(id)) {
196                 adjusting.set(true);
197                 getChildren().clear();
198                 adjusting.set(false);
199                 selectedNodeId.set(null);
200                 selectedNode.set(null);
201             else {
202                 Node node = nodes.get(id);
203                 adjusting.set(true);
204                 getChildren().setAll(node);
205                 adjusting.set(false);
206                 selectedNodeId.set(id);
207                 selectedNode.set(node);
208             }
209         });
210     }
211 }