IndexedCardPane.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.IntegerProperty;
020 import javafx.beans.property.ObjectProperty;
021 import javafx.beans.property.ReadOnlyIntegerProperty;
022 import javafx.beans.property.ReadOnlyObjectProperty;
023 import javafx.beans.property.SimpleIntegerProperty;
024 import javafx.beans.property.SimpleObjectProperty;
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.ArrayList;
033 import java.util.List;
034 import java.util.concurrent.atomic.AtomicBoolean;
035 
036 import static griffon.util.GriffonClassUtils.requireState;
037 import static java.util.Objects.requireNonNull;
038 
039 /**
040  @author Andres Almiray
041  @since 2.11.0
042  */
043 public class IndexedCardPane extends StackPane {
044     private static final String ERROR_NODE_NULL = "Argument 'node' must not be null";
045 
046     private final List<Node> nodes = new ArrayList<>();
047     private final IntegerProperty selectedIndex = new SimpleIntegerProperty(this, "selectedIndex", -1);
048     private final ObjectProperty<Node> selectedNode = new SimpleObjectProperty<>(this, "selectedNode");
049     private final AtomicBoolean adjusting = new AtomicBoolean(false);
050 
051     public IndexedCardPane() {
052         getStyleClass().add("indexed-cardpane");
053 
054         getChildren().addListener((ListChangeListener<Node>c -> {
055             if (adjusting.get()) {
056                 return;
057             }
058 
059             while (c.next()) {
060                 if (c.wasAdded()) {
061                     Node lastAddedNode = null;
062                     for (Node node : c.getAddedSubList()) {
063                         if (!nodes.contains(node)) {
064                             nodes.add(node);
065                             lastAddedNode = node;
066                         }
067                     }
068 
069                     if (lastAddedNode != null) {
070                         doShow(indexOf(lastAddedNode));
071                     }
072                 else if (c.wasRemoved()) {
073                     int selectedIndex = getSelectedIndex();
074                     for (Node node : c.getRemoved()) {
075                         if (!nodes.contains(node)) {
076                             continue;
077                         }
078 
079                         int removedIndex = indexOf(node);
080                         nodes.remove(node);
081 
082                         if (removedIndex <= selectedIndex) {
083                             selectedIndex = -1;
084                         }
085                     }
086 
087                     doShow(selectedIndex);
088                 }
089             }
090         });
091 
092         widthProperty().addListener((observable, oldValue, newValue-> updateBoundsInChildren());
093         heightProperty().addListener((observable, oldValue, newValue-> updateBoundsInChildren());
094     }
095 
096     protected void updateBoundsInChildren() {
097         for (Node node : nodes) {
098             updateChildBounds(node);
099         }
100         layout();
101     }
102 
103     protected void updateChildBounds(Node node) {
104         if (node instanceof Region) {
105             Region child = (Regionnode;
106             child.setPrefWidth(getWidth());
107             child.setPrefHeight(getHeight());
108         }
109     }
110 
111     @Nonnull
112     public ReadOnlyIntegerProperty selectedIndexProperty() {
113         return selectedIndex;
114     }
115 
116     @Nonnull
117     public ReadOnlyObjectProperty<Node> selectedNodeProperty() {
118         return selectedNode;
119     }
120 
121     public int getSelectedIndex() {
122         return selectedIndex.get();
123     }
124 
125     @Nullable
126     public Node getSelectedNode() {
127         return selectedNode.get();
128     }
129 
130     public boolean isEmpty() {
131         return nodes.isEmpty();
132     }
133 
134     public int size() {
135         return nodes.size();
136     }
137 
138     public void clear() {
139         nodes.clear();
140         getChildren().clear();
141         selectedIndex.set(-1);
142         selectedNode.set(null);
143     }
144 
145     public int indexOf(@Nonnull Node node) {
146         requireNonNull(node, ERROR_NODE_NULL);
147         return nodes.indexOf(node);
148     }
149 
150     public void add(@Nonnull Node node) {
151         requireNonNull(node, ERROR_NODE_NULL);
152         if (!nodes.contains(node)) {
153             adjusting.set(true);
154             nodes.add(node);
155             adjusting.set(false);
156         }
157         show(indexOf(node));
158     }
159 
160     public void remove(@Nonnull Node node) {
161         requireNonNull(node, ERROR_NODE_NULL);
162         if (!nodes.contains(node)) {
163             return;
164         }
165 
166         int selectedIndex = getSelectedIndex();
167         int removedIndex = indexOf(node);
168         adjusting.set(true);
169         nodes.remove(node);
170         adjusting.set(false);
171 
172         if (removedIndex <= selectedIndex) {
173             doShow(selectedIndex - 1);
174         }
175     }
176 
177     public void first() {
178         if (nodes.size() 0) {
179             show(0);
180         }
181     }
182 
183     public void last() {
184         int size = nodes.size();
185         if (size > 0) {
186             show(size - 1);
187         }
188     }
189 
190     public void next() {
191         int size = nodes.size();
192         if (size > 0) {
193             int index = getSelectedIndex() 1;
194             show(index >= size ? : index);
195         }
196     }
197 
198     public void previous() {
199         int size = nodes.size();
200         if (size > 0) {
201             int index = getSelectedIndex() 1;
202             show(index < ? size - : index);
203         }
204     }
205 
206     public void show(int index) {
207         requireState(index > -1"Argument 'index' must be greater than -1");
208         requireState(index < nodes.size()"Argument 'index' must be less than " + nodes.size());
209         doShow(index);
210     }
211 
212     protected void doShow(final int index) {
213         Platform.runLater(() -> {
214             if (index < 0) {
215                 adjusting.set(true);
216                 getChildren().clear();
217                 adjusting.set(false);
218                 selectedIndex.set(-1);
219                 selectedNode.set(null);
220             else {
221                 Node node = nodes.get(index);
222                 adjusting.set(true);
223                 getChildren().setAll(node);
224                 adjusting.set(false);
225                 selectedIndex.set(index);
226                 selectedNode.set(node);
227             }
228         });
229     }
230 }