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 }