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