Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 09792ef1 authored by Ben Kwa's avatar Ben Kwa
Browse files

Allow multiple range selections using the shift key.

- Introduce an API on MultiSelectManager for starting/ending range
  selections.

- Navigation with the shift key pressed extends the current range
  selection (or starts a new one, if one isn't in progress).

- Navigation without the shift key pressed will end the current range
  selection.

BUG=27124371

Change-Id: Ieddf3ee816812bf5210463536fe63179ef1809ad
parent 1c9f9222
Loading
Loading
Loading
Loading
+18 −2
Original line number Diff line number Diff line
@@ -101,7 +101,6 @@ import com.android.documentsui.model.RootInfo;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations;

import com.google.common.collect.Lists;

import java.lang.annotation.Retention;
@@ -264,7 +263,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
        mSelectionManager.addCallback(selectionListener);

        // Make sure this is done after the RecyclerView is set up.
        mFocusManager = new FocusManager(mRecView, mSelectionManager);
        mFocusManager = new FocusManager(mRecView);

        mModel = new Model();
        mModel.addUpdateListener(mAdapter);
@@ -1262,6 +1261,18 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
            }

            if (mFocusManager.handleKey(doc, keyCode, event)) {
                // Handle range selection adjustments. Extending the selection will adjust the
                // bounds of the in-progress range selection. Each time an unshifted navigation
                // event is received, the range selection is restarted.
                if (shouldExtendSelection(event)) {
                    if (!mSelectionManager.isRangeSelectionActive()) {
                        // Start a range selection if one isn't active
                        mSelectionManager.startRangeSelection(doc.getAdapterPosition());
                    }
                    mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
                } else {
                    mSelectionManager.endRangeSelection();
                }
                return true;
            }

@@ -1272,6 +1283,11 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi

            return false;
        }

        private boolean shouldExtendSelection(KeyEvent event) {
            return Events.isNavigationKeyCode(event.getKeyCode()) &&
                    event.isShiftPressed();
        }
    }

    private final class ModelUpdateListener implements Model.UpdateListener {
+8 −10
Original line number Diff line number Diff line
@@ -33,15 +33,13 @@ class FocusManager implements View.OnFocusChangeListener {
    private RecyclerView mView;
    private RecyclerView.Adapter<?> mAdapter;
    private LinearLayoutManager mLayout;
    private MultiSelectManager mSelectionManager;

    private int mLastFocusPosition = RecyclerView.NO_POSITION;

    public FocusManager(RecyclerView view, MultiSelectManager selectionManager) {
    public FocusManager(RecyclerView view) {
        mView = view;
        mAdapter = view.getAdapter();
        mLayout = (LinearLayoutManager) view.getLayoutManager();
        mSelectionManager = selectionManager;
    }

    /**
@@ -60,13 +58,6 @@ class FocusManager implements View.OnFocusChangeListener {

            if (endPos != RecyclerView.NO_POSITION) {
                focusItem(endPos);
                boolean extendSelection = event.isShiftPressed();

                // Handle any necessary adjustments to selection.
                if (extendSelection) {
                    int startPos = doc.getAdapterPosition();
                    mSelectionManager.selectRange(startPos, endPos);
                }
            }
            // Swallow all navigation keystrokes. Otherwise they go to the app's global
            // key-handler, which will route them back to the DF and cause focus to be reset.
@@ -96,6 +87,13 @@ class FocusManager implements View.OnFocusChangeListener {
        }
    }

    /**
     * @return The adapter position of the last focused item.
     */
    public int getFocusPosition() {
        return mLastFocusPosition;
    }

    /**
     * Finds the destination position where the focus should land for a given navigation event.
     *
+26 −24
Original line number Diff line number Diff line
@@ -370,39 +370,41 @@ public final class MultiSelectManager {
    }

    /**
     * Handle a range selection event.
     * <li> If the MSM is currently in single-select mode, only the last item in the range will
     * actually be selected.
     * <li>If a range selection is not already active, one will be started, and the given range of
     * items will be selected.  The given startPos becomes the anchor for the range selection.
     * <li>If a range selection is already active, the anchor is not changed. The range is extended
     * from its current anchor to endPos.
     * Starts a range selection. If a range selection is already active, this will start a new range
     * selection (which will reset the range anchor).
     *
     * @param startPos
     * @param endPos
     * @param pos The anchor position for the selection range.
     */
    public void selectRange(int startPos, int endPos) {
        // In single-select mode, just select the last item in the range.
        if (mSingleSelect) {
            attemptSelect(mAdapter.getModelId(endPos));
            return;
    void startRangeSelection(int pos) {
      attemptSelect(mAdapter.getModelId(pos));
      setSelectionRangeBegin(pos);
    }

        // In regular (i.e. multi-select) mode
        if (!isRangeSelectionActive()) {
            // If a range selection isn't active, start one up
            attemptSelect(mAdapter.getModelId(startPos));
            setSelectionRangeBegin(startPos);
        }
        // Extend the range selection
        mRanger.snapSelection(endPos);
    /**
     * Sets the end point for the current range selection, started by a call to
     * {@link #startRangeSelection(int)}. This function should only be called when a range selection
     * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be
     * selected.
     *
     * @param pos The new end position for the selection range.
     */
    void snapRangeSelection(int pos) {
        checkNotNull(mRanger);
        mRanger.snapSelection(pos);
        notifySelectionChanged();
    }

    /**
     * Stops an in-progress range selection.
     */
    void endRangeSelection() {
        mRanger = null;
    }

    /**
     * @return Whether or not there is a current range selection active.
     */
    private boolean isRangeSelectionActive() {
    boolean isRangeSelectionActive() {
        return mRanger != null;
    }

+48 −0
Original line number Diff line number Diff line
@@ -189,6 +189,54 @@ public class MultiSelectManagerTest extends AndroidTestCase {
        assertSelection(items.get(20));
    }

    public void testRangeSelection() {
        mManager.startRangeSelection(15);
        mManager.snapRangeSelection(19);
        assertRangeSelection(15, 19);
    }

    public void testRangeSelection_snapExpand() {
        mManager.startRangeSelection(15);
        mManager.snapRangeSelection(19);
        mManager.snapRangeSelection(27);
        assertRangeSelection(15, 27);
    }

    public void testRangeSelection_snapContract() {
        mManager.startRangeSelection(15);
        mManager.snapRangeSelection(27);
        mManager.snapRangeSelection(19);
        assertRangeSelection(15, 19);
    }

    public void testRangeSelection_snapInvert() {
        mManager.startRangeSelection(15);
        mManager.snapRangeSelection(27);
        mManager.snapRangeSelection(3);
        assertRangeSelection(3, 15);
    }

    public void testRangeSelection_multiple() {
        mManager.startRangeSelection(15);
        mManager.snapRangeSelection(27);
        mManager.endRangeSelection();
        mManager.startRangeSelection(42);
        mManager.snapRangeSelection(57);
        assertSelectionSize(29);
        assertRangeSelected(15, 27);
        assertRangeSelected(42, 57);

    }

    public void testRangeSelection_singleSelect() {
        mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null);
        mManager.addCallback(mCallback);
        mManager.startRangeSelection(11);
        mManager.snapRangeSelection(19);
        assertSelectionSize(1);
        assertSelection(items.get(19));
    }

    public void testProvisionalSelection() {
        Selection s = mManager.getSelection();
        assertSelection();