Home | History | Annotate | Download | only in selection
      1 /*
      2  * Copyright 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 
     17 package androidx.recyclerview.selection;
     18 
     19 import static androidx.core.util.Preconditions.checkArgument;
     20 import static androidx.core.util.Preconditions.checkState;
     21 import static androidx.recyclerview.selection.Shared.DEBUG;
     22 import static androidx.recyclerview.selection.Shared.VERBOSE;
     23 
     24 import android.util.Log;
     25 import android.view.MotionEvent;
     26 
     27 import androidx.annotation.NonNull;
     28 import androidx.annotation.Nullable;
     29 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
     30 import androidx.recyclerview.widget.RecyclerView;
     31 
     32 /**
     33  * A MotionInputHandler that provides the high-level glue for mouse driven selection. This
     34  * class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper}
     35  * to implement the primary policies around mouse input.
     36  */
     37 final class MouseInputHandler<K> extends MotionInputHandler<K> {
     38 
     39     private static final String TAG = "MouseInputDelegate";
     40 
     41     private final ItemDetailsLookup<K> mDetailsLookup;
     42     private final OnContextClickListener mOnContextClickListener;
     43     private final OnItemActivatedListener<K> mOnItemActivatedListener;
     44     private final FocusDelegate<K> mFocusDelegate;
     45 
     46     // The event has been handled in onSingleTapUp
     47     private boolean mHandledTapUp;
     48     // true when the previous event has consumed a right click motion event
     49     private boolean mHandledOnDown;
     50 
     51     MouseInputHandler(
     52             @NonNull SelectionTracker<K> selectionTracker,
     53             @NonNull ItemKeyProvider<K> keyProvider,
     54             @NonNull ItemDetailsLookup<K> detailsLookup,
     55             @NonNull OnContextClickListener onContextClickListener,
     56             @NonNull OnItemActivatedListener<K> onItemActivatedListener,
     57             @NonNull FocusDelegate<K> focusDelegate) {
     58 
     59         super(selectionTracker, keyProvider, focusDelegate);
     60 
     61         checkArgument(detailsLookup != null);
     62         checkArgument(onContextClickListener != null);
     63         checkArgument(onItemActivatedListener != null);
     64 
     65         mDetailsLookup = detailsLookup;
     66         mOnContextClickListener = onContextClickListener;
     67         mOnItemActivatedListener = onItemActivatedListener;
     68         mFocusDelegate = focusDelegate;
     69     }
     70 
     71     @Override
     72     public boolean onDown(@NonNull MotionEvent e) {
     73         if (VERBOSE) Log.v(TAG, "Delegated onDown event.");
     74         if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryMouseButtonPressed(e))
     75                 || MotionEvents.isSecondaryMouseButtonPressed(e)) {
     76             mHandledOnDown = true;
     77             return onRightClick(e);
     78         }
     79 
     80         return false;
     81     }
     82 
     83     @Override
     84     public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
     85             float distanceX, float distanceY) {
     86         // Don't scroll content window in response to mouse drag
     87         // If it's two-finger trackpad scrolling, we want to scroll
     88         return !MotionEvents.isTouchpadScroll(e2);
     89     }
     90 
     91     @Override
     92     public boolean onSingleTapUp(@NonNull MotionEvent e) {
     93         // See b/27377794. Since we don't get a button state back from UP events, we have to
     94         // explicitly save this state to know whether something was previously handled by
     95         // DOWN events or not.
     96         if (mHandledOnDown) {
     97             if (VERBOSE) Log.v(TAG, "Ignoring onSingleTapUp, previously handled in onDown.");
     98             mHandledOnDown = false;
     99             return false;
    100         }
    101 
    102         if (!mDetailsLookup.overItemWithSelectionKey(e)) {
    103             if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
    104             mSelectionTracker.clearSelection();
    105             mFocusDelegate.clearFocus();
    106             return false;
    107         }
    108 
    109         if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
    110             if (DEBUG) Log.d(TAG, "Ignoring middle click");
    111             return false;
    112         }
    113 
    114         if (mSelectionTracker.hasSelection()) {
    115             onItemClick(e, mDetailsLookup.getItemDetails(e));
    116             mHandledTapUp = true;
    117             return true;
    118         }
    119 
    120         return false;
    121     }
    122 
    123     // tap on an item when there is an existing selection. We could extend
    124     // a selection, we could clear selection (then launch)
    125     private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) {
    126         checkState(mSelectionTracker.hasSelection());
    127         checkArgument(item != null);
    128 
    129         if (isRangeExtension(e)) {
    130             extendSelectionRange(item);
    131         } else {
    132             if (shouldClearSelection(e, item)) {
    133                 mSelectionTracker.clearSelection();
    134             }
    135             if (mSelectionTracker.isSelected(item.getSelectionKey())) {
    136                 if (mSelectionTracker.deselect(item.getSelectionKey())) {
    137                     mFocusDelegate.clearFocus();
    138                 }
    139             } else {
    140                 selectOrFocusItem(item, e);
    141             }
    142         }
    143     }
    144 
    145     @Override
    146     public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
    147         if (mHandledTapUp) {
    148             if (VERBOSE) {
    149                 Log.v(TAG,
    150                         "Ignoring onSingleTapConfirmed, previously handled in onSingleTapUp.");
    151             }
    152             mHandledTapUp = false;
    153             return false;
    154         }
    155 
    156         if (mSelectionTracker.hasSelection()) {
    157             return false;  // should have been handled by onSingleTapUp.
    158         }
    159 
    160         if (!mDetailsLookup.overItem(e)) {
    161             if (DEBUG) Log.d(TAG, "Ignoring Confirmed Tap on non-item.");
    162             return false;
    163         }
    164 
    165         if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
    166             if (DEBUG) Log.d(TAG, "Ignoring middle click");
    167             return false;
    168         }
    169 
    170         @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
    171         if (item == null || !item.hasSelectionKey()) {
    172             return false;
    173         }
    174 
    175         if (mFocusDelegate.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) {
    176             mSelectionTracker.startRange(mFocusDelegate.getFocusedPosition());
    177             mSelectionTracker.extendRange(item.getPosition());
    178         } else {
    179             selectOrFocusItem(item, e);
    180         }
    181         return true;
    182     }
    183 
    184     @Override
    185     public boolean onDoubleTap(@NonNull MotionEvent e) {
    186         mHandledTapUp = false;
    187 
    188         if (!mDetailsLookup.overItemWithSelectionKey(e)) {
    189             if (DEBUG) Log.d(TAG, "Ignoring DoubleTap on non-model-backed item.");
    190             return false;
    191         }
    192 
    193         if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
    194             if (DEBUG) Log.d(TAG, "Ignoring middle click");
    195             return false;
    196         }
    197 
    198         ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
    199         return (item != null) && mOnItemActivatedListener.onItemActivated(item, e);
    200     }
    201 
    202     private boolean onRightClick(@NonNull MotionEvent e) {
    203         if (mDetailsLookup.overItemWithSelectionKey(e)) {
    204             @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
    205             if (item != null && !mSelectionTracker.isSelected(item.getSelectionKey())) {
    206                 mSelectionTracker.clearSelection();
    207                 selectItem(item);
    208             }
    209         }
    210 
    211         // We always delegate final handling of the event,
    212         // since the handler might want to show a context menu
    213         // in an empty area or some other weirdo view.
    214         return mOnContextClickListener.onContextClick(e);
    215     }
    216 
    217     private void selectOrFocusItem(@NonNull ItemDetails<K> item, @NonNull MotionEvent e) {
    218         if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) {
    219             selectItem(item);
    220         } else {
    221             focusItem(item);
    222         }
    223     }
    224 }
    225