1 /* 2 * Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control; 27 28 import com.sun.javafx.scene.control.skin.Utils; 29 import javafx.collections.ListChangeListener; 30 import javafx.collections.ObservableList; 31 import javafx.event.Event; 32 import javafx.scene.Node; 33 import javafx.scene.Parent; 34 import javafx.scene.Scene; 35 36 import java.util.List; 37 import java.util.Optional; 38 import java.util.function.Function; 39 import java.util.stream.Collectors; 40 41 class ControlUtils { 42 private ControlUtils() { } 43 44 public static void scrollToIndex(final Control control, int index) { 45 Utils.executeOnceWhenPropertyIsNonNull(control.skinProperty(), (Skin<?> skin) -> { 46 Event.fireEvent(control, new ScrollToEvent<>(control, control, ScrollToEvent.scrollToTopIndex(), index)); 47 }); 48 } 49 50 public static void scrollToColumn(final Control control, final TableColumnBase<?, ?> column) { 51 Utils.executeOnceWhenPropertyIsNonNull(control.skinProperty(), (Skin<?> skin) -> { 52 control.fireEvent(new ScrollToEvent<TableColumnBase<?, ?>>(control, control, ScrollToEvent.scrollToColumn(), column)); 53 }); 54 } 55 56 static void requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(Control c) { 57 runOnFocusedNodeIfChildOf(c, matchingNode -> { 58 matchingNode.requestFocus(); 59 return null; 60 }); 61 } 62 63 static boolean isFocusOnNodeOrAnyChild(final Node n) { 64 return runOnFocusedNodeIfChildOf(n, matchingNode -> true).orElse(false); 65 } 66 67 static <R> Optional<R> runOnFocusedNodeIfChildOf(final Node n, Function<Node, R> f) { 68 if (n == null) return Optional.empty(); 69 if (n.isFocused()) { 70 return Optional.ofNullable(f.apply(n)); 71 } 72 73 Scene scene = n.getScene(); 74 final Node focusOwner = scene == null ? null : scene.getFocusOwner(); 75 76 if (focusOwner == null) { 77 return Optional.empty(); 78 } else if (n.equals(focusOwner)) { 79 return Optional.ofNullable(f.apply(n)); 80 } else { 81 Parent p = focusOwner.getParent(); 82 while (p != null) { 83 if (n.equals(p)) { 84 return Optional.ofNullable(f.apply(p)); 85 } 86 p = p.getParent(); 87 } 88 } 89 return Optional.empty(); 90 } 91 92 static <T> ListChangeListener.Change<T> buildClearAndSelectChange( 93 ObservableList<T> list, List<T> removed, int retainedRow) { 94 return new ListChangeListener.Change<T>(list) { 95 private final int[] EMPTY_PERM = new int[0]; 96 97 private final int removedSize = removed.size(); 98 99 private final List<T> firstRemovedRange; 100 private final List<T> secondRemovedRange; 101 102 private boolean invalid = true; 103 private boolean atFirstRange = true; 104 105 private int from = -1; 106 107 { 108 int midIndex = retainedRow >= removedSize ? removedSize : 109 retainedRow < 0 ? 0 : 110 retainedRow; 111 firstRemovedRange = removed.subList(0, midIndex); 112 secondRemovedRange = removed.subList(midIndex, removedSize); 113 } 114 115 @Override public int getFrom() { 116 checkState(); 117 return from; 118 } 119 120 @Override public int getTo() { 121 return getFrom(); 122 } 123 124 @Override public List<T> getRemoved() { 125 checkState(); 126 return atFirstRange ? firstRemovedRange : secondRemovedRange; 127 } 128 129 @Override public int getRemovedSize() { 130 return atFirstRange ? firstRemovedRange.size() : secondRemovedRange.size(); 131 } 132 133 @Override protected int[] getPermutation() { 134 checkState(); 135 return EMPTY_PERM; 136 } 137 138 @Override public boolean next() { 139 if (invalid && atFirstRange) { 140 invalid = false; 141 142 // point 'from' to the first position, relative to 143 // the underlying selectedCells index. 144 from = 0; 145 return true; 146 } 147 148 if (atFirstRange && !secondRemovedRange.isEmpty()) { 149 atFirstRange = false; 150 151 // point 'from' to the second position, relative to 152 // the underlying selectedCells index. 153 from = 1; 154 return true; 155 } 156 157 return false; 158 } 159 160 @Override public void reset() { 161 invalid = true; 162 atFirstRange = true; 163 } 164 165 private void checkState() { 166 if (invalid) { 167 throw new IllegalStateException("Invalid Change state: next() must be called before inspecting the Change."); 168 } 169 } 170 }; 171 } 172 173 public static <S> void updateSelectedIndices(MultipleSelectionModelBase<S> sm, ListChangeListener.Change<? extends TablePositionBase<?>> c) { 174 sm.selectedIndices._beginChange(); 175 176 while (c.next()) { 177 // it may look like all we are doing here is collecting the removed elements (and 178 // counting the added elements), but the call to 'peek' is also crucial - it is 179 // ensuring that the selectedIndices bitset is correctly updated. 180 181 sm.startAtomic(); 182 final List<Integer> removed = c.getRemoved().stream() 183 .map(TablePositionBase::getRow) 184 .distinct() 185 .peek(sm.selectedIndices::clear) 186 .collect(Collectors.toList()); 187 188 final int addedSize = (int)c.getAddedSubList().stream() 189 .map(TablePositionBase::getRow) 190 .distinct() 191 .peek(sm.selectedIndices::set) 192 .count(); 193 sm.stopAtomic(); 194 195 final int to = c.getFrom() + addedSize; 196 197 if (c.wasReplaced()) { 198 sm.selectedIndices._nextReplace(c.getFrom(), to, removed); 199 } else if (c.wasRemoved()) { 200 sm.selectedIndices._nextRemove(c.getFrom(), removed); 201 } else if (c.wasAdded()) { 202 sm.selectedIndices._nextAdd(c.getFrom(), to); 203 } 204 } 205 c.reset(); 206 sm.selectedIndices.reset(); 207 208 if (sm.isAtomic()) { 209 return; 210 } 211 212 // Fix for RT-31577 - the selectedItems list was going to 213 // empty, but the selectedItem property was staying non-null. 214 // There is a unit test for this, so if a more elegant solution 215 // can be found in the future and this code removed, the unit 216 // test will fail if it isn't fixed elsewhere. 217 // makeAtomic toggle added to resolve RT-32618 218 if (sm.getSelectedItems().isEmpty() && sm.getSelectedItem() != null) { 219 sm.setSelectedItem(null); 220 } 221 222 sm.selectedIndices._endChange(); 223 } 224 225 // Given a listen of removed elements, we create the minimal number of changes by coalescing elements that are 226 // adjacent 227 static void reducingChange(MultipleSelectionModelBase<?>.SelectedIndicesList selectedIndices, List<Integer> removed) { 228 if (removed.isEmpty()) return; 229 230 int startPos = 0; 231 int endPos = 1; 232 boolean firedOnce = false; 233 while (endPos < removed.size()) { 234 if (removed.get(startPos) == removed.get(endPos) - 1) { 235 endPos++; 236 continue; 237 } 238 selectedIndices._nextRemove(selectedIndices.indexOf(removed.get(startPos)), removed.subList(startPos, endPos)); 239 startPos = endPos; 240 endPos = startPos + 1; 241 firedOnce = true; 242 } 243 244 if (!firedOnce) { 245 selectedIndices._nextRemove(selectedIndices.indexOf(removed.get(0)), removed); 246 } 247 } 248 }