1 /*
   2  * Copyright (c) 2013, 2016, 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.ArrayList;
  37 import java.util.List;
  38 import java.util.stream.Collectors;
  39 
  40 class ControlUtils {
  41     private ControlUtils() { }
  42 
  43     public static void scrollToIndex(final Control control, int index) {
  44         Utils.executeOnceWhenPropertyIsNonNull(control.skinProperty(), (Skin<?> skin) -> {
  45             Event.fireEvent(control, new ScrollToEvent<>(control, control, ScrollToEvent.scrollToTopIndex(), index));
  46         });
  47     }
  48 
  49     public static void scrollToColumn(final Control control, final TableColumnBase<?, ?> column) {
  50         Utils.executeOnceWhenPropertyIsNonNull(control.skinProperty(), (Skin<?> skin) -> {
  51             control.fireEvent(new ScrollToEvent<TableColumnBase<?, ?>>(control, control, ScrollToEvent.scrollToColumn(), column));
  52         });
  53     }
  54 
  55     static void requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(Control c) {
  56         Scene scene = c.getScene();
  57         final Node focusOwner = scene == null ? null : scene.getFocusOwner();
  58         if (focusOwner == null) {
  59             c.requestFocus();
  60         } else if (! c.equals(focusOwner)) {
  61             Parent p = focusOwner.getParent();
  62             while (p != null) {
  63                 if (c.equals(p)) {
  64                     c.requestFocus();
  65                     break;
  66                 }
  67                 p = p.getParent();
  68             }
  69         }
  70     }
  71 
  72     static <T> ListChangeListener.Change<T> buildClearAndSelectChange(
  73             ObservableList<T> list, List<T> removed, int retainedRow) {
  74         return new ListChangeListener.Change<T>(list) {
  75             private final int[] EMPTY_PERM = new int[0];
  76 
  77             private final int removedSize = removed.size();
  78 
  79             private final List<T> firstRemovedRange;
  80             private final List<T> secondRemovedRange;
  81 
  82             private boolean invalid = true;
  83             private boolean atFirstRange = true;
  84 
  85             private int from = -1;
  86 
  87             {
  88                 int midIndex = retainedRow >= removedSize ? removedSize :
  89                                retainedRow < 0 ? 0 :
  90                                retainedRow;
  91                 firstRemovedRange = removed.subList(0, midIndex);
  92                 secondRemovedRange = removed.subList(midIndex, removedSize);
  93             }
  94 
  95             @Override public int getFrom() {
  96                 checkState();
  97                 return from;
  98             }
  99 
 100             @Override public int getTo() {
 101                 return getFrom();
 102             }
 103 
 104             @Override public List<T> getRemoved() {
 105                 checkState();
 106                 return atFirstRange ? firstRemovedRange : secondRemovedRange;
 107             }
 108 
 109             @Override public int getRemovedSize() {
 110                 return atFirstRange ? firstRemovedRange.size() : secondRemovedRange.size();
 111             }
 112 
 113             @Override protected int[] getPermutation() {
 114                 checkState();
 115                 return EMPTY_PERM;
 116             }
 117 
 118             @Override public boolean next() {
 119                 if (invalid && atFirstRange) {
 120                     invalid = false;
 121 
 122                     // point 'from' to the first position, relative to
 123                     // the underlying selectedCells index.
 124                     from = 0;
 125                     return true;
 126                 }
 127 
 128                 if (atFirstRange && !secondRemovedRange.isEmpty()) {
 129                     atFirstRange = false;
 130 
 131                     // point 'from' to the second position, relative to
 132                     // the underlying selectedCells index.
 133                     from = 1;
 134                     return true;
 135                 }
 136 
 137                 return false;
 138             }
 139 
 140             @Override public void reset() {
 141                 invalid = true;
 142                 atFirstRange = true;
 143             }
 144 
 145             private void checkState() {
 146                 if (invalid) {
 147                     throw new IllegalStateException("Invalid Change state: next() must be called before inspecting the Change.");
 148                 }
 149             }
 150         };
 151     }
 152 
 153     public static <S> void updateSelectedIndices(MultipleSelectionModelBase<S> sm, ListChangeListener.Change<? extends TablePositionBase<?>> c) {
 154         sm.selectedIndices._beginChange();
 155 
 156         while (c.next()) {
 157             // it may look like all we are doing here is collecting the removed elements (and
 158             // counting the added elements), but the call to 'peek' is also crucial - it is
 159             // ensuring that the selectedIndices bitset is correctly updated.
 160 
 161             sm.startAtomic();
 162             final List<Integer> removed = c.getRemoved().stream()
 163                     .map(TablePositionBase::getRow)
 164                     .distinct()
 165                     .peek(sm.selectedIndices::clear)
 166                     .collect(Collectors.toList());
 167 
 168             final int addedSize = (int)c.getAddedSubList().stream()
 169                     .map(TablePositionBase::getRow)
 170                     .distinct()
 171                     .peek(sm.selectedIndices::set)
 172                     .count();
 173             sm.stopAtomic();
 174 
 175             final int to = c.getFrom() + addedSize;
 176 
 177             if (c.wasReplaced()) {
 178                 sm.selectedIndices._nextReplace(c.getFrom(), to, removed);
 179             } else if (c.wasRemoved()) {
 180                 sm.selectedIndices._nextRemove(c.getFrom(), removed);
 181             } else if (c.wasAdded()) {
 182                 sm.selectedIndices._nextAdd(c.getFrom(), to);
 183             }
 184         }
 185         c.reset();
 186         sm.selectedIndices.reset();
 187 
 188         if (sm.isAtomic()) {
 189             return;
 190         }
 191 
 192         // Fix for RT-31577 - the selectedItems list was going to
 193         // empty, but the selectedItem property was staying non-null.
 194         // There is a unit test for this, so if a more elegant solution
 195         // can be found in the future and this code removed, the unit
 196         // test will fail if it isn't fixed elsewhere.
 197         // makeAtomic toggle added to resolve RT-32618
 198         if (sm.getSelectedItems().isEmpty() && sm.getSelectedItem() != null) {
 199             sm.setSelectedItem(null);
 200         }
 201 
 202         sm.selectedIndices._endChange();
 203     }
 204 
 205     // Given a listen of removed elements, we create the minimal number of changes by coalescing elements that are
 206     // adjacent
 207     static void reducingChange(MultipleSelectionModelBase<?>.SelectedIndicesList selectedIndices, List<Integer> removed) {
 208         if (removed.isEmpty()) return;
 209 
 210         int startPos = 0;
 211         int endPos = 1;
 212         boolean firedOnce = false;
 213         while (endPos < removed.size()) {
 214             if (removed.get(startPos) == removed.get(endPos) - 1) {
 215                 endPos++;
 216                 continue;
 217             }
 218             selectedIndices._nextRemove(selectedIndices.indexOf(removed.get(startPos)), removed.subList(startPos, endPos));
 219             startPos = endPos;
 220             endPos = startPos + 1;
 221             firedOnce = true;
 222         }
 223 
 224         if (!firedOnce) {
 225             selectedIndices._nextRemove(selectedIndices.indexOf(removed.get(0)), removed);
 226         }
 227     }
 228 }