Bug 894045 - Add gamepad support for search suggestions. r=lucasr
authorBrian Nicholson <bnicholson@mozilla.com>
Fri, 26 Jul 2013 11:21:39 -0700
changeset 143457 962211a2b765b4306a65c44d22df6a30283a8c7f
parent 143456 d42a6a047a7a0c5c55963cba5732ea69d04f4d66
child 143458 6c4b716fe1898ff347380dbe935754f8d6bae6cc
push id25130
push userlrocha@mozilla.com
push dateWed, 21 Aug 2013 09:41:27 +0000
treeherdermozilla-central@b2486721572e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs894045
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 894045 - Add gamepad support for search suggestions. r=lucasr
mobile/android/base/home/BrowserSearch.java
mobile/android/base/home/SearchEngineRow.java
mobile/android/base/resources/layout/home_search_item_row.xml
mobile/android/base/resources/layout/search_engine_row.xml
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -234,16 +234,32 @@ public class BrowserSearch extends HomeF
                     return;
                 }
 
                 final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
                 mUrlOpenListener.onUrlOpen(url);
             }
         });
 
+        final ListSelectionListener listener = new ListSelectionListener();
+        mList.setOnItemSelectedListener(listener);
+        mList.setOnFocusChangeListener(listener);
+
+        mList.setOnKeyListener(new View.OnKeyListener() {
+            @Override
+            public boolean onKey(View v, int keyCode, android.view.KeyEvent event) {
+                final View selected = mList.getSelectedView();
+
+                if (selected instanceof SearchEngineRow) {
+                    return selected.onKeyDown(keyCode, event);
+                }
+                return false;
+            }
+        });
+
         registerForContextMenu(mList);
         registerEventListener("SearchEngines:Data");
 
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null));
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
@@ -429,16 +445,28 @@ public class BrowserSearch extends HomeF
                 noButton.setOnClickListener(null);
 
                 setSuggestionsEnabled(v == yesButton);
             }
         };
 
         yesButton.setOnClickListener(listener);
         noButton.setOnClickListener(listener);
+
+        // If the prompt gains focus, automatically pass focus to the
+        // yes button in the prompt.
+        final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt);
+        prompt.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+            @Override
+            public void onFocusChange(View v, boolean hasFocus) {
+                if (hasFocus) {
+                    yesButton.requestFocus();
+                }
+            }
+        });
     }
 
     private void setSuggestionsEnabled(final boolean enabled) {
         // Clicking the yes/no buttons quickly can cause the click events be
         // queued before the listeners are removed above, so it's possible
         // setSuggestionsEnabled() can be called twice. mSuggestionsOptInPrompt
         // can be null if this happens (bug 828480).
         if (mSuggestionsOptInPrompt == null) {
@@ -627,16 +655,22 @@ public class BrowserSearch extends HomeF
         public int getViewTypeCount() {
             // view can be either a standard awesomebar row, a search engine
             // row, or a suggestion row
             return ROW_TYPE_COUNT;
         }
 
         @Override
         public boolean isEnabled(int position) {
+            // If we're using a gamepad or keyboard, allow the row to be
+            // focused so it can pass the focus to its child suggestion views.
+            if (!mList.isInTouchMode()) {
+                return true;
+            }
+
             // If the suggestion row only contains one item (the user-entered
             // query), allow the entire row to be clickable; clicking the row
             // has the same effect as clicking the single suggestion. If the
             // row contains multiple items, clicking the row will do nothing.
             final int index = getEngineIndex(position);
             if (index != -1) {
                 return mSearchEngines.get(index).suggestions.isEmpty();
             }
@@ -796,9 +830,51 @@ public class BrowserSearch extends HomeF
             setSuggestions(suggestions);
         }
 
         @Override
         public void onLoaderReset(Loader<ArrayList<String>> loader) {
             setSuggestions(new ArrayList<String>());
         }
     }
+
+    private static class ListSelectionListener implements View.OnFocusChangeListener,
+                                                          AdapterView.OnItemSelectedListener {
+        private SearchEngineRow mSelectedEngineRow;
+
+        @Override
+        public void onFocusChange(View v, boolean hasFocus) {
+            if (hasFocus) {
+                View selectedRow = ((ListView) v).getSelectedView();
+                if (selectedRow != null) {
+                    selectRow(selectedRow);
+                }
+            } else {
+                deselectRow();
+            }
+        }
+
+        @Override
+        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+            deselectRow();
+            selectRow(view);
+        }
+
+        @Override
+        public void onNothingSelected(AdapterView<?> parent) {
+            deselectRow();
+        }
+
+        private void selectRow(View row) {
+            if (row instanceof SearchEngineRow) {
+                mSelectedEngineRow = (SearchEngineRow) row;
+                mSelectedEngineRow.onSelected();
+            }
+        }
+
+        private void deselectRow() {
+            if (mSelectedEngineRow != null) {
+                mSelectedEngineRow.onDeselected();
+                mSelectedEngineRow = null;
+            }
+        }
+    }
 }
\ No newline at end of file
--- a/mobile/android/base/home/SearchEngineRow.java
+++ b/mobile/android/base/home/SearchEngineRow.java
@@ -11,16 +11,17 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.home.BrowserSearch.OnEditSuggestionListener;
 import org.mozilla.gecko.home.BrowserSearch.OnSearchListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.widget.FaviconView;
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.animation.AlphaAnimation;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 class SearchEngineRow extends AnimatedHeightLayout {
@@ -47,16 +48,19 @@ class SearchEngineRow extends AnimatedHe
     private OnUrlOpenListener mUrlOpenListener;
 
     // On search listener
     private OnSearchListener mSearchListener;
 
     // On edit suggestion listener
     private OnEditSuggestionListener mEditSuggestionListener;
 
+    // Selected suggestion view
+    private int mSelectedView = 0;
+
     public SearchEngineRow(Context context) {
         this(context, null);
     }
 
     public SearchEngineRow(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
@@ -174,10 +178,69 @@ class SearchEngineRow extends AnimatedHe
                 suggestionItem.startAnimation(anim);
             }
         }
 
         // Hide extra suggestions that have been recycled
         for (int i = suggestionCount + 1; i < recycledSuggestionCount; i++) {
             mSuggestionView.getChildAt(i).setVisibility(View.GONE);
         }
+
+        // Make sure mSelectedView is still valid
+        if (mSelectedView >= mSuggestionView.getChildCount()) {
+            mSelectedView = mSuggestionView.getChildCount() - 1;
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, android.view.KeyEvent event) {
+        final View suggestion = mSuggestionView.getChildAt(mSelectedView);
+
+        if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) {
+            return false;
+        }
+
+        switch (event.getKeyCode()) {
+        case KeyEvent.KEYCODE_DPAD_RIGHT:
+            final View nextSuggestion = mSuggestionView.getChildAt(mSelectedView + 1);
+            if (nextSuggestion != null) {
+                changeSelectedSuggestion(suggestion, nextSuggestion);
+                mSelectedView++;
+                return true;
+            }
+            break;
+
+        case KeyEvent.KEYCODE_DPAD_LEFT:
+            final View prevSuggestion = mSuggestionView.getChildAt(mSelectedView - 1);
+            if (prevSuggestion != null) {
+                changeSelectedSuggestion(suggestion, prevSuggestion);
+                mSelectedView--;
+                return true;
+            }
+            break;
+
+        case KeyEvent.KEYCODE_BUTTON_A:
+            // TODO: handle long pressing for editing suggestions
+            return suggestion.performClick();
+        }
+
+        return false;
+    }
+
+    private void changeSelectedSuggestion(View oldSuggestion, View newSuggestion) {
+        oldSuggestion.setDuplicateParentStateEnabled(false);
+        newSuggestion.setDuplicateParentStateEnabled(true);
+        oldSuggestion.refreshDrawableState();
+        newSuggestion.refreshDrawableState();
+    }
+
+    public void onSelected() {
+        mSelectedView = 0;
+        mUserEnteredView.setDuplicateParentStateEnabled(true);
+        mUserEnteredView.refreshDrawableState();
+    }
+
+    public void onDeselected() {
+        final View suggestion = mSuggestionView.getChildAt(mSelectedView);
+        suggestion.setDuplicateParentStateEnabled(false);
+        suggestion.refreshDrawableState();
     }
 }
--- a/mobile/android/base/resources/layout/home_search_item_row.xml
+++ b/mobile/android/base/resources/layout/home_search_item_row.xml
@@ -2,10 +2,11 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <org.mozilla.gecko.home.SearchEngineRow xmlns:android="http://schemas.android.com/apk/res/android"
                                         android:layout_width="fill_parent"
                                         android:layout_height="wrap_content"
                                         android:minHeight="@dimen/search_row_height"
+                                        android:duplicateParentState="true"
                                         android:paddingTop="7dp"
                                         android:paddingBottom="7dp"/>
--- a/mobile/android/base/resources/layout/search_engine_row.xml
+++ b/mobile/android/base/resources/layout/search_engine_row.xml
@@ -1,29 +1,30 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
-  <org.mozilla.gecko.widget.FaviconView android:id="@+id/suggestion_icon"
-                                        android:layout_width="@dimen/favicon_bg"
-                                        android:layout_height="@dimen/favicon_bg"
-                                        android:layout_marginLeft="10dip"
-                                        android:layout_marginRight="10dip"
-                                        android:layout_centerVertical="true"
-                                        android:minWidth="@dimen/favicon_bg"
-                                        android:minHeight="@dimen/favicon_bg"/>
+    <org.mozilla.gecko.widget.FaviconView android:id="@+id/suggestion_icon"
+                                          android:layout_width="@dimen/favicon_bg"
+                                          android:layout_height="@dimen/favicon_bg"
+                                          android:layout_marginLeft="10dip"
+                                          android:layout_marginRight="10dip"
+                                          android:layout_centerVertical="true"
+                                          android:minWidth="@dimen/favicon_bg"
+                                          android:minHeight="@dimen/favicon_bg"/>
 
     <org.mozilla.gecko.FlowLayout android:id="@+id/suggestion_layout"
                                   android:layout_toRightOf="@id/suggestion_icon"
                                   android:layout_centerVertical="true"
                                   android:layout_width="wrap_content"
                                   android:layout_height="wrap_content"
+                                  android:duplicateParentState="true"
                                   android:layout_marginRight="10dip">
 
-    <include layout="@layout/suggestion_item"
-             android:id="@+id/suggestion_user_entered"/>
+        <include layout="@layout/suggestion_item"
+                 android:id="@+id/suggestion_user_entered"/>
 
     </org.mozilla.gecko.FlowLayout>
 
 </merge>