1 /* 2 * Copyright (c) 2010, 2015, 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 com.sun.javafx.scene.control.behavior; 27 28 import javafx.scene.control.Cell; 29 import javafx.scene.control.Control; 30 import javafx.scene.control.FocusModel; 31 import javafx.scene.control.IndexedCell; 32 import javafx.scene.control.MultipleSelectionModel; 33 import javafx.scene.control.SelectionMode; 34 import javafx.scene.input.ContextMenuEvent; 35 import javafx.scene.input.MouseButton; 36 import javafx.scene.input.MouseEvent; 37 38 import java.util.ArrayList; 39 import java.util.List; 40 41 /** 42 * Behaviors for standard cells types. Simply defines methods that subclasses 43 * implement so that CellSkinBase has API to call. 44 */ 45 public abstract class CellBehaviorBase<T extends Cell> extends BehaviorBase<T> { 46 47 48 /*************************************************************************** 49 * * 50 * Private static implementation * 51 * * 52 **************************************************************************/ 53 54 private static final String ANCHOR_PROPERTY_KEY = "anchor"; 55 56 // The virtualised controls all start with selection on row 0 by default. 57 // This means that we have a default anchor, but it should be removed if 58 // a different anchor could be set - and normally we ignore the default 59 // anchor anyway. 60 private static final String IS_DEFAULT_ANCHOR_KEY = "isDefaultAnchor"; 61 62 public static <T> T getAnchor(Control control, T defaultResponse) { 63 return hasNonDefaultAnchor(control) ? 64 (T) control.getProperties().get(ANCHOR_PROPERTY_KEY) : 65 defaultResponse; 66 } 67 68 public static <T> void setAnchor(Control control, T anchor, boolean isDefaultAnchor) { 69 if (control != null && anchor == null) { 70 removeAnchor(control); 71 } else { 72 control.getProperties().put(ANCHOR_PROPERTY_KEY, anchor); 73 control.getProperties().put(IS_DEFAULT_ANCHOR_KEY, isDefaultAnchor); 74 } 75 } 76 77 public static boolean hasNonDefaultAnchor(Control control) { 78 Boolean isDefaultAnchor = (Boolean) control.getProperties().remove(IS_DEFAULT_ANCHOR_KEY); 79 return (isDefaultAnchor == null || isDefaultAnchor == false) && hasAnchor(control); 80 } 81 82 public static boolean hasDefaultAnchor(Control control) { 83 Boolean isDefaultAnchor = (Boolean) control.getProperties().remove(IS_DEFAULT_ANCHOR_KEY); 84 return isDefaultAnchor != null && isDefaultAnchor == true && hasAnchor(control); 85 } 86 87 private static boolean hasAnchor(Control control) { 88 return control.getProperties().get(ANCHOR_PROPERTY_KEY) != null; 89 } 90 91 public static void removeAnchor(Control control) { 92 control.getProperties().remove(ANCHOR_PROPERTY_KEY); 93 control.getProperties().remove(IS_DEFAULT_ANCHOR_KEY); 94 } 95 96 97 98 /*************************************************************************** 99 * * 100 * Private fields * 101 * * 102 **************************************************************************/ 103 104 // To support touch devices, we have to slightly modify this behavior, such 105 // that selection only happens on mouse release, if only minimal dragging 106 // has occurred. 107 private boolean latePress = false; 108 109 110 111 /*************************************************************************** 112 * * 113 * Constructors * 114 * * 115 **************************************************************************/ 116 117 public CellBehaviorBase(T control, List<KeyBinding> bindings) { 118 super(control, bindings); 119 } 120 121 122 protected abstract Control getCellContainer(); // e.g. ListView 123 protected abstract MultipleSelectionModel<?> getSelectionModel(); 124 protected abstract FocusModel<?> getFocusModel(); 125 protected abstract void edit(T cell); 126 protected boolean handleDisclosureNode(double x, double y) { 127 return false; 128 } 129 protected boolean isClickPositionValid(final double x, final double y) { 130 return true; 131 } 132 133 134 /*************************************************************************** 135 * * 136 * Public API * 137 * * 138 **************************************************************************/ 139 140 protected int getIndex() { 141 return getControl() instanceof IndexedCell ? ((IndexedCell<?>)getControl()).getIndex() : -1; 142 } 143 144 @Override public void mousePressed(MouseEvent e) { 145 if (e.isSynthesized()) { 146 latePress = true; 147 } else { 148 latePress = isSelected(); 149 if (!latePress) { 150 doSelect(e.getX(), e.getY(), e.getButton(), e.getClickCount(), 151 e.isShiftDown(), e.isShortcutDown()); 152 } 153 } 154 } 155 156 @Override public void mouseReleased(MouseEvent e) { 157 if (latePress) { 158 latePress = false; 159 doSelect(e.getX(), e.getY(), e.getButton(), e.getClickCount(), 160 e.isShiftDown(), e.isShortcutDown()); 161 } 162 } 163 164 @Override public void mouseDragged(MouseEvent e) { 165 latePress = false; 166 } 167 168 169 170 /*************************************************************************** 171 * * 172 * Private implementation * 173 * * 174 **************************************************************************/ 175 176 protected void doSelect(final double x, final double y, final MouseButton button, 177 final int clickCount, final boolean shiftDown, final boolean shortcutDown) { 178 // we update the cell to point to the new tree node 179 final T cell = getControl(); 180 181 final Control cellContainer = getCellContainer(); 182 183 // If the mouse event is not contained within this TreeCell, then 184 // we don't want to react to it. 185 if (cell.isEmpty() || ! cell.contains(x, y)) { 186 return; 187 } 188 189 final int index = getIndex(); 190 boolean selected = cell.isSelected(); 191 MultipleSelectionModel<?> sm = getSelectionModel(); 192 if (sm == null) return; 193 194 FocusModel<?> fm = getFocusModel(); 195 if (fm == null) return; 196 197 // if the user has clicked on the disclosure node, we do nothing other 198 // than expand/collapse the tree item (if applicable). We do not do editing! 199 if (handleDisclosureNode(x,y)) { 200 return; 201 } 202 203 // we only care about clicks in certain places (depending on the subclass) 204 if (! isClickPositionValid(x, y)) return; 205 206 // if shift is down, and we don't already have the initial focus index 207 // recorded, we record the focus index now so that subsequent shift+clicks 208 // result in the correct selection occuring (whilst the focus index moves 209 // about). 210 if (shiftDown) { 211 if (! hasNonDefaultAnchor(cellContainer)) { 212 setAnchor(cellContainer, fm.getFocusedIndex(), false); 213 } 214 } else { 215 removeAnchor(cellContainer); 216 } 217 218 if (button == MouseButton.PRIMARY || (button == MouseButton.SECONDARY && !selected)) { 219 if (sm.getSelectionMode() == SelectionMode.SINGLE) { 220 simpleSelect(button, clickCount, shortcutDown); 221 } else { 222 if (shortcutDown) { 223 if (selected) { 224 // we remove this row from the current selection 225 sm.clearSelection(index); 226 fm.focus(index); 227 } else { 228 // We add this row to the current selection 229 sm.select(index); 230 } 231 } else if (shiftDown && clickCount == 1) { 232 // we add all rows between the current selection focus and 233 // this row (inclusive) to the current selection. 234 final int focusedIndex = getAnchor(cellContainer, fm.getFocusedIndex()); 235 236 selectRows(focusedIndex, index); 237 238 fm.focus(index); 239 } else { 240 simpleSelect(button, clickCount, shortcutDown); 241 } 242 } 243 } 244 } 245 246 protected void simpleSelect(MouseButton button, int clickCount, boolean shortcutDown) { 247 final int index = getIndex(); 248 MultipleSelectionModel<?> sm = getSelectionModel(); 249 boolean isAlreadySelected = sm.isSelected(index); 250 251 if (isAlreadySelected && shortcutDown) { 252 sm.clearSelection(index); 253 getFocusModel().focus(index); 254 isAlreadySelected = false; 255 } else { 256 sm.clearAndSelect(index); 257 } 258 259 handleClicks(button, clickCount, isAlreadySelected); 260 } 261 262 protected void handleClicks(MouseButton button, int clickCount, boolean isAlreadySelected) { 263 // handle editing, which only occurs with the primary mouse button 264 if (button == MouseButton.PRIMARY) { 265 if (clickCount == 1 && isAlreadySelected) { 266 edit(getControl()); 267 } else if (clickCount == 1) { 268 // cancel editing 269 edit(null); 270 } else if (clickCount == 2 && getControl().isEditable()) { 271 edit(getControl()); 272 } 273 } 274 } 275 276 void selectRows(int focusedIndex, int index) { 277 final boolean asc = focusedIndex < index; 278 279 // and then determine all row and columns which must be selected 280 int minRow = Math.min(focusedIndex, index); 281 int maxRow = Math.max(focusedIndex, index); 282 283 // To prevent RT-32119, we make a copy of the selected indices 284 // list first, so that we are not iterating and modifying it 285 // concurrently. 286 List<Integer> selectedIndices = new ArrayList<>(getSelectionModel().getSelectedIndices()); 287 for (int i = 0, max = selectedIndices.size(); i < max; i++) { 288 int selectedIndex = selectedIndices.get(i); 289 if (selectedIndex < minRow || selectedIndex > maxRow) { 290 getSelectionModel().clearSelection(selectedIndex); 291 } 292 } 293 294 if (minRow == maxRow) { 295 // RT-32560: This prevents the anchor 'sticking' in 296 // the wrong place when a range is selected and then 297 // selection goes back to the anchor position. 298 // (Refer to the video in RT-32560 for more detail). 299 getSelectionModel().select(minRow); 300 } else { 301 // RT-21444: We need to put the range in the correct 302 // order or else the last selected row will not be the 303 // last item in the selectedItems list of the selection 304 // model, 305 if (asc) { 306 getSelectionModel().selectRange(minRow, maxRow + 1); 307 } else { 308 getSelectionModel().selectRange(maxRow, minRow - 1); 309 } 310 } 311 } 312 313 protected boolean isSelected() { 314 return getControl().isSelected(); 315 } 316 }