/*
* Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.javafx.scene.control.behavior;
import javafx.scene.control.Cell;
import javafx.scene.control.Control;
import javafx.scene.control.FocusModel;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.SelectionMode;
import com.sun.javafx.scene.control.inputmap.InputMap;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import java.util.ArrayList;
import java.util.List;
Behaviors for standard cells types. Simply defines methods that subclasses
implement so that CellSkinBase has API to call.
/**
* Behaviors for standard cells types. Simply defines methods that subclasses
* implement so that CellSkinBase has API to call.
*/
public abstract class CellBehaviorBase<T extends Cell> extends BehaviorBase<T> {
*
Private static implementation *
*
/***************************************************************************
* *
* Private static implementation *
* *
**************************************************************************/
private static final String ANCHOR_PROPERTY_KEY = "anchor";
// The virtualised controls all start with selection on row 0 by default.
// This means that we have a default anchor, but it should be removed if
// a different anchor could be set - and normally we ignore the default
// anchor anyway.
private static final String IS_DEFAULT_ANCHOR_KEY = "isDefaultAnchor";
public static <T> T getAnchor(Control control, T defaultResponse) {
return hasNonDefaultAnchor(control) ?
(T) control.getProperties().get(ANCHOR_PROPERTY_KEY) :
defaultResponse;
}
public static <T> void setAnchor(Control control, T anchor, boolean isDefaultAnchor) {
if (control == null) return;
if (anchor == null) {
removeAnchor(control);
} else {
control.getProperties().put(ANCHOR_PROPERTY_KEY, anchor);
control.getProperties().put(IS_DEFAULT_ANCHOR_KEY, isDefaultAnchor);
}
}
public static boolean hasNonDefaultAnchor(Control control) {
Boolean isDefaultAnchor = (Boolean) control.getProperties().remove(IS_DEFAULT_ANCHOR_KEY);
return (isDefaultAnchor == null || isDefaultAnchor == false) && hasAnchor(control);
}
public static boolean hasDefaultAnchor(Control control) {
Boolean isDefaultAnchor = (Boolean) control.getProperties().remove(IS_DEFAULT_ANCHOR_KEY);
return isDefaultAnchor != null && isDefaultAnchor == true && hasAnchor(control);
}
private static boolean hasAnchor(Control control) {
return control.getProperties().get(ANCHOR_PROPERTY_KEY) != null;
}
public static void removeAnchor(Control control) {
control.getProperties().remove(ANCHOR_PROPERTY_KEY);
control.getProperties().remove(IS_DEFAULT_ANCHOR_KEY);
}
*
Private fields *
*
/***************************************************************************
* *
* Private fields *
* *
**************************************************************************/
private final InputMap<T> cellInputMap;
// To support touch devices, we have to slightly modify this behavior, such
// that selection only happens on mouse release, if only minimal dragging
// has occurred.
private boolean latePress = false;
*
Constructors *
*
/***************************************************************************
* *
* Constructors *
* *
**************************************************************************/
public CellBehaviorBase(T control) {
super(control);
// create a map for cell-specific mappings (this reuses the default
// InputMap installed on the control, if it is non-null, allowing us to pick up any user-specified mappings)
cellInputMap = createInputMap();
// TODO add focus traversal mappings (?)
// addDefaultMapping(cellInputMap, FocusTraversalInputMap.getFocusTraversalMappings());
InputMap.MouseMapping pressedMapping, releasedMapping, mouseDragged;
addDefaultMapping(
pressedMapping = new InputMap.MouseMapping(MouseEvent.MOUSE_PRESSED, this::mousePressed),
releasedMapping = new InputMap.MouseMapping(MouseEvent.MOUSE_RELEASED, this::mouseReleased),
mouseDragged = new InputMap.MouseMapping(MouseEvent.MOUSE_DRAGGED, this::mouseDragged)
);
pressedMapping.setAutoConsume(false);
releasedMapping.setAutoConsume(false);
mouseDragged.setAutoConsume(false);
}
protected abstract Control getCellContainer(); // e.g. ListView
protected abstract MultipleSelectionModel<?> getSelectionModel();
protected abstract FocusModel<?> getFocusModel();
protected abstract void edit(T cell);
protected boolean handleDisclosureNode(double x, double y) {
return false;
}
protected boolean isClickPositionValid(final double x, final double y) {
return true;
}
/***************************************************************************
* *
* Public API *
* *
**************************************************************************/
{@inheritDoc} /** {@inheritDoc} */
@Override public InputMap<T> getInputMap() {
return cellInputMap;
}
protected int getIndex() {
return getNode() instanceof IndexedCell ? ((IndexedCell<?>)getNode()).getIndex() : -1;
}
public void mousePressed(MouseEvent e) {
if (e.isSynthesized()) {
latePress = true;
} else {
latePress = isSelected();
if (!latePress) {
doSelect(e.getX(), e.getY(), e.getButton(), e.getClickCount(),
e.isShiftDown(), e.isShortcutDown());
}
}
}
public void mouseReleased(MouseEvent e) {
if (latePress) {
latePress = false;
doSelect(e.getX(), e.getY(), e.getButton(), e.getClickCount(),
e.isShiftDown(), e.isShortcutDown());
}
}
public void mouseDragged(MouseEvent e) {
latePress = false;
}
*
Private implementation *
*
/***************************************************************************
* *
* Private implementation *
* *
**************************************************************************/
protected void doSelect(final double x, final double y, final MouseButton button,
final int clickCount, final boolean shiftDown, final boolean shortcutDown) {
// we update the cell to point to the new tree node
final T cell = getNode();
final Control cellContainer = getCellContainer();
// If the mouse event is not contained within this TreeCell, then
// we don't want to react to it.
if (cell.isEmpty() || ! cell.contains(x, y)) {
return;
}
final int index = getIndex();
boolean selected = cell.isSelected();
MultipleSelectionModel<?> sm = getSelectionModel();
if (sm == null) return;
FocusModel<?> fm = getFocusModel();
if (fm == null) return;
// if the user has clicked on the disclosure node, we do nothing other
// than expand/collapse the tree item (if applicable). We do not do editing!
if (handleDisclosureNode(x,y)) {
return;
}
// we only care about clicks in certain places (depending on the subclass)
if (! isClickPositionValid(x, y)) return;
// if shift is down, and we don't already have the initial focus index
// recorded, we record the focus index now so that subsequent shift+clicks
// result in the correct selection occuring (whilst the focus index moves
// about).
if (shiftDown) {
if (! hasNonDefaultAnchor(cellContainer)) {
setAnchor(cellContainer, fm.getFocusedIndex(), false);
}
} else {
removeAnchor(cellContainer);
}
if (button == MouseButton.PRIMARY || (button == MouseButton.SECONDARY && !selected)) {
if (sm.getSelectionMode() == SelectionMode.SINGLE) {
simpleSelect(button, clickCount, shortcutDown);
} else {
if (shortcutDown) {
if (selected) {
// we remove this row from the current selection
sm.clearSelection(index);
fm.focus(index);
} else {
// We add this row to the current selection
sm.select(index);
}
} else if (shiftDown && clickCount == 1) {
// we add all rows between the current selection focus and
// this row (inclusive) to the current selection.
final int focusedIndex = getAnchor(cellContainer, fm.getFocusedIndex());
selectRows(focusedIndex, index);
fm.focus(index);
} else {
simpleSelect(button, clickCount, shortcutDown);
}
}
}
}
protected void simpleSelect(MouseButton button, int clickCount, boolean shortcutDown) {
final int index = getIndex();
MultipleSelectionModel<?> sm = getSelectionModel();
boolean isAlreadySelected = sm.isSelected(index);
if (isAlreadySelected && shortcutDown) {
sm.clearSelection(index);
getFocusModel().focus(index);
isAlreadySelected = false;
} else {
sm.clearAndSelect(index);
}
handleClicks(button, clickCount, isAlreadySelected);
}
protected void handleClicks(MouseButton button, int clickCount, boolean isAlreadySelected) {
// handle editing, which only occurs with the primary mouse button
if (button == MouseButton.PRIMARY) {
if (clickCount == 1 && isAlreadySelected) {
edit(getNode());
} else if (clickCount == 1) {
// cancel editing
edit(null);
} else if (clickCount == 2 && getNode().isEditable()) {
edit(getNode());
}
}
}
void selectRows(int focusedIndex, int index) {
final boolean asc = focusedIndex < index;
// and then determine all row and columns which must be selected
int minRow = Math.min(focusedIndex, index);
int maxRow = Math.max(focusedIndex, index);
// To prevent RT-32119, we make a copy of the selected indices
// list first, so that we are not iterating and modifying it
// concurrently.
List<Integer> selectedIndices = new ArrayList<>(getSelectionModel().getSelectedIndices());
for (int i = 0, max = selectedIndices.size(); i < max; i++) {
int selectedIndex = selectedIndices.get(i);
if (selectedIndex < minRow || selectedIndex > maxRow) {
getSelectionModel().clearSelection(selectedIndex);
}
}
if (minRow == maxRow) {
// RT-32560: This prevents the anchor 'sticking' in
// the wrong place when a range is selected and then
// selection goes back to the anchor position.
// (Refer to the video in RT-32560 for more detail).
getSelectionModel().select(minRow);
} else {
// RT-21444: We need to put the range in the correct
// order or else the last selected row will not be the
// last item in the selectedItems list of the selection
// model,
if (asc) {
getSelectionModel().selectRange(minRow, maxRow + 1);
} else {
getSelectionModel().selectRange(maxRow, minRow - 1);
}
}
}
protected boolean isSelected() {
return getNode().isSelected();
}
}