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 }