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