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 = (Region) node;
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 ? 0 : 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 < 0 ? size - 1 : 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 }
|