163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay/* 263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * Copyright 2017 The Android Open Source Project 363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * 463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * Licensed under the Apache License, Version 2.0 (the "License"); 563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * you may not use this file except in compliance with the License. 663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * You may obtain a copy of the License at 763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * 863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * http://www.apache.org/licenses/LICENSE-2.0 963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * 1063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * Unless required by applicable law or agreed to in writing, software 1163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * distributed under the License is distributed on an "AS IS" BASIS, 1263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * See the License for the specific language governing permissions and 1463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * limitations under the License. 1563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay */ 1663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 172a32c7e1264b14a20ed900abadea828b804a46ceAurimas Liutikaspackage androidx.recyclerview.selection; 1863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 19ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport static androidx.core.util.Preconditions.checkArgument; 20ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport static androidx.core.util.Preconditions.checkState; 212a32c7e1264b14a20ed900abadea828b804a46ceAurimas Liutikasimport static androidx.recyclerview.selection.Shared.DEBUG; 222a32c7e1264b14a20ed900abadea828b804a46ceAurimas Liutikasimport static androidx.recyclerview.selection.Shared.VERBOSE; 2363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 2463d2846409d84487d4856d3b8d18cc4684352e29Steve McKayimport android.util.Log; 2563d2846409d84487d4856d3b8d18cc4684352e29Steve McKayimport android.view.MotionEvent; 2663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 2760dadaeed4f5cee272b575dfde6c02e3506a2fa0Aurimas Liutikasimport androidx.annotation.NonNull; 2860dadaeed4f5cee272b575dfde6c02e3506a2fa0Aurimas Liutikasimport androidx.annotation.Nullable; 292a32c7e1264b14a20ed900abadea828b804a46ceAurimas Liutikasimport androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; 3060dadaeed4f5cee272b575dfde6c02e3506a2fa0Aurimas Liutikasimport androidx.recyclerview.widget.RecyclerView; 3163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 3263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay/** 33e48623efafef695e2fd0bab51f57c6dbeb24edf3Steve McKay * A MotionInputHandler that provides the high-level glue for mouse driven selection. This 3463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay * class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} 357fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay * to implement the primary policies around mouse input. 3663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay */ 3763d2846409d84487d4856d3b8d18cc4684352e29Steve McKayfinal class MouseInputHandler<K> extends MotionInputHandler<K> { 3863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 3963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay private static final String TAG = "MouseInputDelegate"; 4063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 4163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay private final ItemDetailsLookup<K> mDetailsLookup; 427fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay private final OnContextClickListener mOnContextClickListener; 437fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay private final OnItemActivatedListener<K> mOnItemActivatedListener; 447fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay private final FocusDelegate<K> mFocusDelegate; 4563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 4663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // The event has been handled in onSingleTapUp 4763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay private boolean mHandledTapUp; 4863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // true when the previous event has consumed a right click motion event 4963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay private boolean mHandledOnDown; 5063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 5163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay MouseInputHandler( 527fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay @NonNull SelectionTracker<K> selectionTracker, 537fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay @NonNull ItemKeyProvider<K> keyProvider, 547fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay @NonNull ItemDetailsLookup<K> detailsLookup, 557fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay @NonNull OnContextClickListener onContextClickListener, 567fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay @NonNull OnItemActivatedListener<K> onItemActivatedListener, 577fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay @NonNull FocusDelegate<K> focusDelegate) { 5863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 597fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay super(selectionTracker, keyProvider, focusDelegate); 6063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 6163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay checkArgument(detailsLookup != null); 627fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay checkArgument(onContextClickListener != null); 637fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay checkArgument(onItemActivatedListener != null); 6463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 6563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay mDetailsLookup = detailsLookup; 667fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mOnContextClickListener = onContextClickListener; 677fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mOnItemActivatedListener = onItemActivatedListener; 687fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mFocusDelegate = focusDelegate; 6963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 7063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 7163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay @Override 727fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay public boolean onDown(@NonNull MotionEvent e) { 7363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (VERBOSE) Log.v(TAG, "Delegated onDown event."); 747fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryMouseButtonPressed(e)) 757fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay || MotionEvents.isSecondaryMouseButtonPressed(e)) { 7663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay mHandledOnDown = true; 7763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return onRightClick(e); 7863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 7963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 8063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 8163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 8263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 8363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay @Override 847fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, 857fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay float distanceX, float distanceY) { 8663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // Don't scroll content window in response to mouse drag 8763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // If it's two-finger trackpad scrolling, we want to scroll 8863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return !MotionEvents.isTouchpadScroll(e2); 8963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 9063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 9163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay @Override 927fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay public boolean onSingleTapUp(@NonNull MotionEvent e) { 9363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // See b/27377794. Since we don't get a button state back from UP events, we have to 9463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // explicitly save this state to know whether something was previously handled by 9563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // DOWN events or not. 9663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (mHandledOnDown) { 9763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (VERBOSE) Log.v(TAG, "Ignoring onSingleTapUp, previously handled in onDown."); 9863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay mHandledOnDown = false; 9963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 10063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 10163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 10263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (!mDetailsLookup.overItemWithSelectionKey(e)) { 10363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection."); 1047fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mSelectionTracker.clearSelection(); 1057fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mFocusDelegate.clearFocus(); 10663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 10763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 10863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 1097fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if (MotionEvents.isTertiaryMouseButtonPressed(e)) { 11063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (DEBUG) Log.d(TAG, "Ignoring middle click"); 11163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 11263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 11363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 1147fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if (mSelectionTracker.hasSelection()) { 11563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay onItemClick(e, mDetailsLookup.getItemDetails(e)); 11663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay mHandledTapUp = true; 11763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return true; 11863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 11963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 12063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 12163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 12263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 12363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // tap on an item when there is an existing selection. We could extend 12463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // a selection, we could clear selection (then launch) 1257fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) { 1267fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay checkState(mSelectionTracker.hasSelection()); 12763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay checkArgument(item != null); 12863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 12963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (isRangeExtension(e)) { 13063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay extendSelectionRange(item); 13163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } else { 13263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (shouldClearSelection(e, item)) { 1337fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mSelectionTracker.clearSelection(); 13463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 1357fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if (mSelectionTracker.isSelected(item.getSelectionKey())) { 1367fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if (mSelectionTracker.deselect(item.getSelectionKey())) { 1377fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mFocusDelegate.clearFocus(); 13863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 13963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } else { 14063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay selectOrFocusItem(item, e); 14163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 14263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 14363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 14463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 14563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay @Override 1467fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay public boolean onSingleTapConfirmed(@NonNull MotionEvent e) { 14763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (mHandledTapUp) { 14863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (VERBOSE) { 14963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay Log.v(TAG, 15063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay "Ignoring onSingleTapConfirmed, previously handled in onSingleTapUp."); 15163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 15263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay mHandledTapUp = false; 15363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 15463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 15563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 1567fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if (mSelectionTracker.hasSelection()) { 15763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; // should have been handled by onSingleTapUp. 15863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 15963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 16063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (!mDetailsLookup.overItem(e)) { 16163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (DEBUG) Log.d(TAG, "Ignoring Confirmed Tap on non-item."); 16263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 16363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 16463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 1657fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if (MotionEvents.isTertiaryMouseButtonPressed(e)) { 16663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (DEBUG) Log.d(TAG, "Ignoring middle click"); 16763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 16863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 16963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 17063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e); 17163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (item == null || !item.hasSelectionKey()) { 17263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 17363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 17463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 1757fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if (mFocusDelegate.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) { 1767fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mSelectionTracker.startRange(mFocusDelegate.getFocusedPosition()); 1777fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mSelectionTracker.extendRange(item.getPosition()); 17863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } else { 17963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay selectOrFocusItem(item, e); 18063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 18163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return true; 18263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 18363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 18463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay @Override 1857fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay public boolean onDoubleTap(@NonNull MotionEvent e) { 18663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay mHandledTapUp = false; 18763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 18863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (!mDetailsLookup.overItemWithSelectionKey(e)) { 18963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (DEBUG) Log.d(TAG, "Ignoring DoubleTap on non-model-backed item."); 19063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 19163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 19263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 1937fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if (MotionEvents.isTertiaryMouseButtonPressed(e)) { 19463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (DEBUG) Log.d(TAG, "Ignoring middle click"); 19563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay return false; 19663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 19763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 19863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay ItemDetails<K> item = mDetailsLookup.getItemDetails(e); 1997fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay return (item != null) && mOnItemActivatedListener.onItemActivated(item, e); 20063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 20163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 2027fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay private boolean onRightClick(@NonNull MotionEvent e) { 20363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (mDetailsLookup.overItemWithSelectionKey(e)) { 20463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e); 2057fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay if (item != null && !mSelectionTracker.isSelected(item.getSelectionKey())) { 2067fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay mSelectionTracker.clearSelection(); 20763d2846409d84487d4856d3b8d18cc4684352e29Steve McKay selectItem(item); 20863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 20963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 21063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 21163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // We always delegate final handling of the event, 21263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // since the handler might want to show a context menu 21363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay // in an empty area or some other weirdo view. 2147fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay return mOnContextClickListener.onContextClick(e); 21563d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 21663d2846409d84487d4856d3b8d18cc4684352e29Steve McKay 2177fb763509e07f98d650efc25c91bff8b1cb239acSteve McKay private void selectOrFocusItem(@NonNull ItemDetails<K> item, @NonNull MotionEvent e) { 21863d2846409d84487d4856d3b8d18cc4684352e29Steve McKay if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) { 21963d2846409d84487d4856d3b8d18cc4684352e29Steve McKay selectItem(item); 22063d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } else { 22163d2846409d84487d4856d3b8d18cc4684352e29Steve McKay focusItem(item); 22263d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 22363d2846409d84487d4856d3b8d18cc4684352e29Steve McKay } 22463d2846409d84487d4856d3b8d18cc4684352e29Steve McKay} 225