1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.documentsui.selection; 17 18 import static com.android.documentsui.selection.Shared.DEBUG; 19 import static com.android.documentsui.selection.Shared.VERBOSE; 20 21 import android.util.Log; 22 import android.view.MotionEvent; 23 24 import com.android.documentsui.selection.ItemDetailsLookup.ItemDetails; 25 import com.android.internal.widget.RecyclerView; 26 27 import javax.annotation.Nullable; 28 29 /** 30 * A MotionInputHandler that provides the high-level glue for mouse/stylus driven selection. This 31 * class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} 32 * to provide robust user drive selection support. 33 */ 34 public final class MouseInputHandler extends MotionInputHandler { 35 36 private static final String TAG = "MouseInputDelegate"; 37 38 private final Callbacks mCallbacks; 39 40 // The event has been handled in onSingleTapUp 41 private boolean mHandledTapUp; 42 // true when the previous event has consumed a right click motion event 43 private boolean mHandledOnDown; 44 MouseInputHandler( SelectionHelper selectionHelper, ItemDetailsLookup detailsLookup, Callbacks callbacks)45 public MouseInputHandler( 46 SelectionHelper selectionHelper, 47 ItemDetailsLookup detailsLookup, 48 Callbacks callbacks) { 49 50 super(selectionHelper, detailsLookup, callbacks); 51 52 mCallbacks = callbacks; 53 } 54 55 @Override onDown(MotionEvent e)56 public boolean onDown(MotionEvent e) { 57 if (VERBOSE) Log.v(TAG, "Delegated onDown event."); 58 if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryButtonPressed(e)) 59 || MotionEvents.isSecondaryButtonPressed(e)) { 60 mHandledOnDown = true; 61 return onRightClick(e); 62 } 63 64 return false; 65 } 66 67 @Override onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)68 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 69 // Don't scroll content window in response to mouse drag 70 // If it's two-finger trackpad scrolling, we want to scroll 71 return !MotionEvents.isTouchpadScroll(e2); 72 } 73 74 @Override onSingleTapUp(MotionEvent e)75 public boolean onSingleTapUp(MotionEvent e) { 76 // See b/27377794. Since we don't get a button state back from UP events, we have to 77 // explicitly save this state to know whether something was previously handled by 78 // DOWN events or not. 79 if (mHandledOnDown) { 80 if (VERBOSE) Log.v(TAG, "Ignoring onSingleTapUp, previously handled in onDown."); 81 mHandledOnDown = false; 82 return false; 83 } 84 85 if (!mDetailsLookup.overStableItem(e)) { 86 if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection."); 87 mSelectionHelper.clearSelection(); 88 mCallbacks.clearFocus(); 89 return false; 90 } 91 92 if (MotionEvents.isTertiaryButtonPressed(e)) { 93 if (DEBUG) Log.d(TAG, "Ignoring middle click"); 94 return false; 95 } 96 97 ItemDetails item = mDetailsLookup.getItemDetails(e); 98 if (mSelectionHelper.hasSelection()) { 99 if (isRangeExtension(e)) { 100 extendSelectionRange(item); 101 } else { 102 if (shouldClearSelection(e, item)) { 103 mSelectionHelper.clearSelection(); 104 } 105 if (mSelectionHelper.isSelected(item.getStableId())) { 106 if (mSelectionHelper.deselect(item.getStableId())) { 107 mCallbacks.clearFocus(); 108 } 109 } else { 110 selectOrFocusItem(item, e); 111 } 112 } 113 mHandledTapUp = true; 114 return true; 115 } 116 117 return false; 118 } 119 120 @Override onSingleTapConfirmed(MotionEvent e)121 public boolean onSingleTapConfirmed(MotionEvent e) { 122 if (mHandledTapUp) { 123 if (VERBOSE) Log.v(TAG, 124 "Ignoring onSingleTapConfirmed, previously handled in onSingleTapUp."); 125 mHandledTapUp = false; 126 return false; 127 } 128 129 if (mSelectionHelper.hasSelection()) { 130 return false; // should have been handled by onSingleTapUp. 131 } 132 133 if (!mDetailsLookup.overItem(e)) { 134 if (DEBUG) Log.d(TAG, "Ignoring Confirmed Tap on non-item."); 135 return false; 136 } 137 138 if (MotionEvents.isTertiaryButtonPressed(e)) { 139 if (DEBUG) Log.d(TAG, "Ignoring middle click"); 140 return false; 141 } 142 143 @Nullable ItemDetails item = mDetailsLookup.getItemDetails(e); 144 if (item == null || !item.hasStableId()) { 145 Log.w(TAG, "Ignoring Confirmed Tap. No document details associated w/ event."); 146 return false; 147 } 148 149 if (mCallbacks.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) { 150 mSelectionHelper.startRange(mCallbacks.getFocusedPosition()); 151 mSelectionHelper.extendRange(item.getPosition()); 152 } else { 153 selectOrFocusItem(item, e); 154 } 155 return true; 156 } 157 158 @Override onDoubleTap(MotionEvent e)159 public boolean onDoubleTap(MotionEvent e) { 160 mHandledTapUp = false; 161 162 if (!mDetailsLookup.overStableItem(e)) { 163 if (DEBUG) Log.d(TAG, "Ignoring DoubleTap on non-model-backed item."); 164 return false; 165 } 166 167 if (MotionEvents.isTertiaryButtonPressed(e)) { 168 if (DEBUG) Log.d(TAG, "Ignoring middle click"); 169 return false; 170 } 171 172 @Nullable ItemDetails item = mDetailsLookup.getItemDetails(e); 173 if (item != null) { 174 return mCallbacks.onItemActivated(item, e); 175 } 176 177 return false; 178 } 179 onRightClick(MotionEvent e)180 private boolean onRightClick(MotionEvent e) { 181 if (mDetailsLookup.overStableItem(e)) { 182 @Nullable ItemDetails item = mDetailsLookup.getItemDetails(e); 183 if (item != null && !mSelectionHelper.isSelected(item.getStableId())) { 184 mSelectionHelper.clearSelection(); 185 selectItem(item); 186 } 187 } 188 189 // We always delegate final handling of the event, 190 // since the handler might want to show a context menu 191 // in an empty area or some other weirdo view. 192 return mCallbacks.onContextClick(e); 193 } 194 selectOrFocusItem(ItemDetails item, MotionEvent e)195 private void selectOrFocusItem(ItemDetails item, MotionEvent e) { 196 if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) { 197 selectItem(item); 198 } else { 199 focusItem(item); 200 } 201 } 202 203 public static abstract class Callbacks extends MotionInputHandler.Callbacks { onItemActivated(ItemDetails item, MotionEvent e)204 public abstract boolean onItemActivated(ItemDetails item, MotionEvent e); onContextClick(MotionEvent e)205 public boolean onContextClick(MotionEvent e) { 206 return false; 207 } hasFocusedItem()208 public boolean hasFocusedItem() { 209 return false; 210 } getFocusedPosition()211 public int getFocusedPosition() { 212 return RecyclerView.NO_POSITION; 213 } 214 } 215 }