Bug 1086983 - Part 1: Restore editing text when switching between tabs in editing mode. r=lucasr
authorMichael Comella <michael.l.comella@gmail.com>
Thu, 06 Nov 2014 09:46:39 -0800
changeset 226074 53d7570e20f2f5b2f0c01519d089755fed48c873
parent 226073 bc84a9d465dbe51698c7cbdfa5768968aca60675
child 226075 36e434d7d9909cc6722d909b69bfa1ebffda5be3
push id32
push userbmcbride@mozilla.com
push dateSun, 09 Nov 2014 12:47:32 +0000
reviewerslucasr
bugs1086983
milestone36.0a1
Bug 1086983 - Part 1: Restore editing text when switching between tabs in editing mode. r=lucasr
mobile/android/base/BrowserApp.java
mobile/android/base/Tab.java
mobile/android/base/toolbar/BrowserToolbar.java
mobile/android/base/toolbar/ToolbarEditLayout.java
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -63,16 +63,17 @@ import org.mozilla.gecko.sync.setup.Sync
 import org.mozilla.gecko.tabs.TabHistoryController;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
 import org.mozilla.gecko.tabs.TabsPanel;
 import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tiles.TilesRecorder;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
+import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.MenuUtils;
 import org.mozilla.gecko.util.NativeEventListener;
@@ -240,16 +241,18 @@ public class BrowserApp extends GeckoApp
 
     private ReadingListHelper mReadingListHelper;
 
     private SystemBarTintManager mTintManager;
 
     // The tab to be selected on editing mode exit.
     private Integer mTargetTabForEditingMode;
 
+    private final TabEditingState mLastTabEditingState = new TabEditingState();
+
     // The animator used to toggle HomePager visibility has a race where if the HomePager is shown
     // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
     // race by determining if the web content should be hidden at the animation's end.
     private boolean mHideWebContentOnAnimationEnd;
 
     private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
 
@@ -308,27 +311,71 @@ public class BrowserApp extends GeckoApp
                 tab.loadFavicon();
                 break;
             case BOOKMARK_ADDED:
                 showBookmarkAddedToast();
                 break;
             case BOOKMARK_REMOVED:
                 showBookmarkRemovedToast();
                 break;
+
+            case UNSELECTED:
+                // We receive UNSELECTED immediately after the SELECTED listeners run
+                // so we are ensured that the unselectedTabEditingText has not changed.
+                if (tab.isEditing()) {
+                    // Copy to avoid constructing new objects.
+                    tab.getEditingState().copyFrom(mLastTabEditingState);
+                }
+                break;
         }
 
         if (NewTabletUI.isEnabled(this) && msg == TabEvents.SELECTED) {
-            // Note: this is a duplicate call if maybeSwitchToTab
-            // is called, though the call is idempotent.
-            mBrowserToolbar.cancelEdit();
+            updateEditingModeForTab(tab);
         }
 
         super.onTabChanged(tab, msg, data);
     }
 
+    private void updateEditingModeForTab(final Tab selectedTab) {
+        // (bug 1086983 comment 11) Because the tab may be selected from the gecko thread and we're
+        // running this code on the UI thread, the selected tab argument may not still refer to the
+        // selected tab. However, that means this code should be run again and the initial state
+        // changes will be overridden. As an optimization, we can skip this update, but it may have
+        // unknown side-effects so we don't.
+        if (!Tabs.getInstance().isSelectedTab(selectedTab)) {
+            Log.w(LOGTAG, "updateEditingModeForTab: Given tab is expected to be selected tab");
+        }
+
+        saveTabEditingState(mLastTabEditingState);
+
+        if (selectedTab.isEditing()) {
+            enterEditingMode();
+            restoreTabEditingState(selectedTab.getEditingState());
+        } else {
+            mBrowserToolbar.cancelEdit();
+        }
+    }
+
+    private void saveTabEditingState(final TabEditingState editingState) {
+        mBrowserToolbar.saveTabEditingState(editingState);
+        editingState.setIsBrowserSearchShown(mBrowserSearch.getUserVisibleHint());
+    }
+
+    private void restoreTabEditingState(final TabEditingState editingState) {
+        mBrowserToolbar.restoreTabEditingState(editingState);
+
+        // Since changing the editing text will show/hide browser search, this
+        // must be called after we restore the editing state in the edit text View.
+        if (editingState.isBrowserSearchShown()) {
+            showBrowserSearch();
+        } else {
+            hideBrowserSearch();
+        }
+    }
+
     private void showBookmarkAddedToast() {
         getButtonToast().show(false,
                 getResources().getString(R.string.bookmark_added),
                 ButtonToast.LENGTH_SHORT,
                 getResources().getString(R.string.bookmark_options),
                 null,
                 new ButtonToast.ToastListener() {
                     @Override
@@ -833,24 +880,34 @@ public class BrowserApp extends GeckoApp
                     mHomePager.onToolbarFocusChange(hasFocus);
                 }
             }
         });
 
         mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() {
             @Override
             public void onStartEditing() {
+                final Tab selectedTab = Tabs.getInstance().getSelectedTab();
+                if (selectedTab != null) {
+                    selectedTab.setIsEditing(true);
+                }
+
                 // Temporarily disable doorhanger notifications.
                 mDoorHangerPopup.disable();
             }
         });
 
         mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() {
             @Override
             public void onStopEditing() {
+                final Tab selectedTab = Tabs.getInstance().getSelectedTab();
+                if (selectedTab != null) {
+                    selectedTab.setIsEditing(false);
+                }
+
                 selectTargetTabForEditingMode();
 
                 // Since the underlying LayerView is set visible in hideHomePager, we would
                 // ordinarily want to call it first. However, hideBrowserSearch changes the
                 // visibility of the HomePager and hideHomePager will take no action if the
                 // HomePager is hidden, so we want to call hideBrowserSearch to restore the
                 // HomePager visibility first.
                 hideBrowserSearch();
@@ -1812,16 +1869,21 @@ public class BrowserApp extends GeckoApp
     private boolean maybeSwitchToTab(int id) {
         final Tabs tabs = Tabs.getInstance();
         final Tab tab = tabs.getTab(id);
 
         if (tab == null) {
             return false;
         }
 
+        final Tab oldTab = tabs.getSelectedTab();
+        if (oldTab != null) {
+            oldTab.setIsEditing(false);
+        }
+
         // Set the target tab to null so it does not get selected (on editing
         // mode exit) in lieu of the tab we are about to select.
         mTargetTabForEditingMode = null;
         tabs.selectTab(tab.getId());
 
         mBrowserToolbar.cancelEdit();
 
         return true;
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -17,16 +17,17 @@ import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.URLMetadata;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.RemoteFavicon;
 import org.mozilla.gecko.gfx.Layer;
+import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 import android.os.Build;
@@ -72,16 +73,19 @@ public class Tab {
     private boolean mDesktopMode;
     private boolean mEnteringReaderMode;
     private final Context mAppContext;
     private ErrorType mErrorType = ErrorType.NONE;
     private volatile int mLoadProgress;
     private volatile int mRecordingCount;
     private String mMostRecentHomePanel;
 
+    private boolean mIsEditing;
+    private final TabEditingState mEditingState = new TabEditingState();
+
     public static final int STATE_DELAYED = 0;
     public static final int STATE_LOADING = 1;
     public static final int STATE_SUCCESS = 2;
     public static final int STATE_ERROR = 3;
 
     public static final int LOAD_PROGRESS_INIT = 10;
     public static final int LOAD_PROGRESS_START = 20;
     public static final int LOAD_PROGRESS_LOCATION_CHANGE = 60;
@@ -840,9 +844,21 @@ public class Tab {
         } else {
             mRecordingCount--;
         }
     }
 
     public boolean isRecording() {
         return mRecordingCount > 0;
     }
+
+    public boolean isEditing() {
+        return mIsEditing;
+    }
+
+    public void setIsEditing(final boolean isEditing) {
+        this.mIsEditing = isEditing;
+    }
+
+    public TabEditingState getEditingState() {
+        return mEditingState;
+    }
 }
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -417,16 +417,28 @@ public abstract class BrowserToolbar ext
                 @Override
                 public void run() {
                     activity.refreshToolbarHeight();
                 }
             });
         }
     }
 
+    public void saveTabEditingState(final TabEditingState editingState) {
+        urlEditLayout.saveTabEditingState(editingState);
+    }
+
+    public void restoreTabEditingState(final TabEditingState editingState) {
+        if (!isEditing()) {
+            throw new IllegalStateException("Expected to be editing");
+        }
+
+        urlEditLayout.restoreTabEditingState(editingState);
+    }
+
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         Log.d(LOGTAG, "onTabChanged: " + msg);
         final Tabs tabs = Tabs.getInstance();
 
         // These conditions are split into three phases:
         // * Always do first
         // * Handling specific to the selected tab
@@ -911,9 +923,34 @@ public abstract class BrowserToolbar ext
         editCancel.onLightweightThemeChanged();
     }
 
     @Override
     public void onLightweightThemeReset() {
         setBackgroundResource(R.drawable.url_bar_bg);
         editCancel.onLightweightThemeReset();
     }
+
+    public static class TabEditingState {
+        // The edited text from the most recent time this tab was unselected.
+        protected String lastEditingText;
+        protected int selectionStart;
+        protected int selectionEnd;
+
+        public boolean isBrowserSearchShown;
+
+        public void copyFrom(final TabEditingState s2) {
+            lastEditingText = s2.lastEditingText;
+            selectionStart = s2.selectionStart;
+            selectionEnd = s2.selectionEnd;
+
+            isBrowserSearchShown = s2.isBrowserSearchShown;
+        }
+
+        public boolean isBrowserSearchShown() {
+            return isBrowserSearchShown;
+        }
+
+        public void setIsBrowserSearchShown(final boolean isShown) {
+            isBrowserSearchShown = isShown;
+        }
+    }
 }
--- a/mobile/android/base/toolbar/ToolbarEditLayout.java
+++ b/mobile/android/base/toolbar/ToolbarEditLayout.java
@@ -6,16 +6,17 @@
 package org.mozilla.gecko.toolbar;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
 import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
 import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
 import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
+import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.widget.ThemedLinearLayout;
 
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.inputmethod.InputMethodManager;
@@ -122,9 +123,20 @@ public class ToolbarEditLayout extends T
 
     void setText(String text) {
         mEditText.setText(text);
     }
 
     String getText() {
         return mEditText.getText().toString();
     }
+
+    protected void saveTabEditingState(final TabEditingState editingState) {
+        editingState.lastEditingText = getText();
+        editingState.selectionStart = mEditText.getSelectionStart();
+        editingState.selectionEnd = mEditText.getSelectionEnd();
+   }
+
+    protected void restoreTabEditingState(final TabEditingState editingState) {
+        mEditText.setText(editingState.lastEditingText);
+        mEditText.setSelection(editingState.selectionStart, editingState.selectionEnd);
+    }
 }