Bug 1246064 - Support long press to show AccessibleCaret on empty input for Fennec. r=roc draft
authorTing-Yu Lin <tlin@mozilla.com>
Mon, 08 Feb 2016 16:08:46 +0800
changeset 329524 d7ba96fb8aaa31d04ffc367a2ddc0fec245fbcbf
parent 329523 ded28e8aa976bdd03b2fa53389af508e41b273d5
child 513969 5c34107cfdcd59edb917d7bf8295b2d527ba8891
push id10537
push usertlin@mozilla.com
push dateMon, 08 Feb 2016 08:20:40 +0000
reviewersroc
bugs1246064
milestone47.0a1
Bug 1246064 - Support long press to show AccessibleCaret on empty input for Fennec. r=roc
layout/base/AccessibleCaretManager.cpp
layout/base/AccessibleCaretManager.h
layout/base/gtest/TestAccessibleCaretManager.cpp
mobile/android/app/mobile.js
modules/libpref/init/all.js
--- a/layout/base/AccessibleCaretManager.cpp
+++ b/layout/base/AccessibleCaretManager.cpp
@@ -62,16 +62,18 @@ std::ostream& operator<<(std::ostream& a
   }
   return aStream;
 }
 #undef AC_PROCESS_ENUM_TO_STREAM
 
 /*static*/ bool
 AccessibleCaretManager::sSelectionBarEnabled = false;
 /*static*/ bool
+AccessibleCaretManager::sCaretShownWhenLongTappingOnEmptyContent = false;
+/*static*/ bool
 AccessibleCaretManager::sCaretsExtendedVisibility = false;
 /*static*/ bool
 AccessibleCaretManager::sCaretsScriptUpdates = false;
 /*static*/ bool
 AccessibleCaretManager::sHapticFeedback = false;
 
 AccessibleCaretManager::AccessibleCaretManager(nsIPresShell* aPresShell)
   : mPresShell(aPresShell)
@@ -84,16 +86,18 @@ AccessibleCaretManager::AccessibleCaretM
   mSecondCaret = MakeUnique<AccessibleCaret>(mPresShell);
 
   mCaretTimeoutTimer = do_CreateInstance("@mozilla.org/timer;1");
 
   static bool addedPrefs = false;
   if (!addedPrefs) {
     Preferences::AddBoolVarCache(&sSelectionBarEnabled,
                                  "layout.accessiblecaret.bar.enabled");
+    Preferences::AddBoolVarCache(&sCaretShownWhenLongTappingOnEmptyContent,
+      "layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content");
     Preferences::AddBoolVarCache(&sCaretsExtendedVisibility,
                                  "layout.accessiblecaret.extendedvisibility");
     Preferences::AddBoolVarCache(&sCaretsScriptUpdates,
       "layout.accessiblecaret.allow_script_change_updates");
     Preferences::AddBoolVarCache(&sHapticFeedback,
                                  "layout.accessiblecaret.hapticfeedback");
     addedPrefs = true;
   }
@@ -262,24 +266,40 @@ AccessibleCaretManager::UpdateCaretsForC
       // Do nothing
       break;
 
     case PositionChangedResult::Changed:
       switch (aHint) {
         case UpdateCaretsHint::Default:
           if (HasNonEmptyTextContent(GetEditingHostForFrame(frame))) {
             mFirstCaret->SetAppearance(Appearance::Normal);
+          } else if (sCaretShownWhenLongTappingOnEmptyContent) {
+            if (mFirstCaret->IsLogicallyVisible()) {
+              // Possible cases are: 1) SelectWordOrShortcut() sets the
+              // appearance to Normal. 2) When the caret is out of viewport and
+              // now scrolling into viewport, it has appearance NormalNotShown.
+              mFirstCaret->SetAppearance(Appearance::Normal);
+            } else {
+              // Possible cases are: a) Single tap on current empty content;
+              // OnSelectionChanged() sets the appearance to None due to
+              // MOUSEDOWN_REASON. b) Single tap on other empty content;
+              // OnBlur() sets the appearance to None.
+              //
+              // Do nothing to make the appearance remains None so that it can
+              // be distinguished from case 2). Also do not set the appearance
+              // to NormalNotShown here like the default update behavior.
+            }
           } else {
             mFirstCaret->SetAppearance(Appearance::NormalNotShown);
           }
           break;
 
         case UpdateCaretsHint::RespectOldAppearance:
-          // Do nothing to prevent the appearance of the caret being
-          // changed from NormalNotShown to Normal.
+          // Do nothing to preserve the appearance of the caret set by the
+          // caller.
           break;
       }
       break;
 
     case PositionChangedResult::Invisible:
       mFirstCaret->SetAppearance(Appearance::NormalNotShown);
       break;
   }
@@ -479,16 +499,20 @@ AccessibleCaretManager::SelectWordOrShor
          focusableFrame ? focusableFrame->ListTag().get() : "no frame");
 #endif
 
   // Firstly check long press on an empty editable content.
   Element* newFocusEditingHost = GetEditingHostForFrame(ptFrame);
   if (focusableFrame && newFocusEditingHost &&
       !HasNonEmptyTextContent(newFocusEditingHost)) {
     ChangeFocusToOrClearOldFocus(focusableFrame);
+
+    if (sCaretShownWhenLongTappingOnEmptyContent) {
+      mFirstCaret->SetAppearance(Appearance::Normal);
+    }
     // We need to update carets to get correct information before dispatching
     // CaretStateChangedEvent.
     UpdateCaretsWithHapticFeedback();
     DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent);
     return NS_OK;
   }
 
   bool selectable = false;
--- a/layout/base/AccessibleCaretManager.h
+++ b/layout/base/AccessibleCaretManager.h
@@ -246,16 +246,22 @@ protected:
   // boundary by 61 app units, which is 1 pixel + 1 app unit as defined in
   // AppUnit.h.
   static const int32_t kBoundaryAppUnits = 61;
 
   // Preference to show selection bars at the two ends in selection mode. The
   // selection bar is always disabled in cursor mode.
   static bool sSelectionBarEnabled;
 
+  // Preference to show caret in cursor mode when long tapping on an empty
+  // content. This also changes the default update behavior in cursor mode,
+  // which is based on the emptiness of the content, into something more
+  // heuristic. See UpdateCaretsForCursorMode() for the details.
+  static bool sCaretShownWhenLongTappingOnEmptyContent;
+
   // Android specific visibility extensions correct compatibility issues
   // with caret-drag and ActionBar visibility during page scroll.
   static bool sCaretsExtendedVisibility;
 
   // By default, javascript content selection changes closes AccessibleCarets and
   // UI interactions. Optionally, we can try to maintain the active UI, keeping
   // carets and ActionBar available.
   static bool sCaretsScriptUpdates;
--- a/layout/base/gtest/TestAccessibleCaretManager.cpp
+++ b/layout/base/gtest/TestAccessibleCaretManager.cpp
@@ -6,16 +6,17 @@
 
 #include "gtest/gtest.h"
 #include "gmock/gmock.h"
 
 #include <string>
 
 #include "AccessibleCaret.h"
 #include "AccessibleCaretManager.h"
+#include "mozilla/AutoRestore.h"
 
 using ::testing::DefaultValue;
 using ::testing::Eq;
 using ::testing::InSequence;
 using ::testing::MockFunction;
 using ::testing::Return;
 using ::testing::_;
 
@@ -53,16 +54,17 @@ public:
   }; // class MockAccessibleCaret
 
   class MockAccessibleCaretManager : public AccessibleCaretManager
   {
   public:
     using CaretMode = AccessibleCaretManager::CaretMode;
     using AccessibleCaretManager::UpdateCarets;
     using AccessibleCaretManager::HideCarets;
+    using AccessibleCaretManager::sCaretShownWhenLongTappingOnEmptyContent;
 
     MockAccessibleCaretManager()
       : AccessibleCaretManager(nullptr)
     {
       mFirstCaret = MakeUnique<MockAccessibleCaret>();
       mSecondCaret = MakeUnique<MockAccessibleCaret>();
     }
 
@@ -489,9 +491,171 @@ TEST_F(AccessibleCaretManagerTester, Tes
   mManager.OnScrollStart();
   EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
 
   mManager.OnScrollEnd();
   EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
   check.Call("scrollend2");
 }
 
+TEST_F(AccessibleCaretManagerTester, TestScrollInCursorModeOnEmptyContent)
+{
+  EXPECT_CALL(mManager, GetCaretMode())
+    .WillRepeatedly(Return(CaretMode::Cursor));
+
+  EXPECT_CALL(mManager, HasNonEmptyTextContent(_))
+    .WillRepeatedly(Return(false));
+
+  MockFunction<void(std::string aCheckPointName)> check;
+  {
+    InSequence dummy;
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                   CaretChangedReason::Updateposition));
+    EXPECT_CALL(check, Call("updatecarets"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                   CaretChangedReason::Visibilitychange));
+    EXPECT_CALL(check, Call("scrollstart1"));
+
+    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
+      .WillOnce(Return(PositionChangedResult::Invisible));
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                   CaretChangedReason::Updateposition));
+    EXPECT_CALL(check, Call("scrollend1"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                   CaretChangedReason::Visibilitychange));
+    EXPECT_CALL(check, Call("scrollstart2"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                   CaretChangedReason::Updateposition));
+    EXPECT_CALL(check, Call("scrollend2"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                   CaretChangedReason::Visibilitychange));
+    EXPECT_CALL(check, Call("scrollstart3"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                   CaretChangedReason::Updateposition));
+    EXPECT_CALL(check, Call("scrollend3"));
+}
+
+  // Simulate a single tap on an empty content.
+  mManager.UpdateCarets();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
+  check.Call("updatecarets");
+
+  // Scroll the caret to be out of the viewport.
+  mManager.OnScrollStart();
+  check.Call("scrollstart1");
+  mManager.OnScrollEnd();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
+  check.Call("scrollend1");
+
+  // Scroll the caret into the viewport.
+  mManager.OnScrollStart();
+  check.Call("scrollstart2");
+  mManager.OnScrollEnd();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
+  check.Call("scrollend2");
+
+  // Scroll the caret within the viewport.
+  mManager.OnScrollStart();
+  check.Call("scrollstart3");
+  mManager.OnScrollEnd();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
+  check.Call("scrollend3");
+}
+
+
+TEST_F(AccessibleCaretManagerTester,
+       TestScrollInCursorModeOnEmptyContentWithSpecialPreference)
+{
+  EXPECT_CALL(mManager, GetCaretMode())
+    .WillRepeatedly(Return(CaretMode::Cursor));
+
+  EXPECT_CALL(mManager, HasNonEmptyTextContent(_))
+    .WillRepeatedly(Return(false));
+
+  MockFunction<void(std::string aCheckPointName)> check;
+  {
+    InSequence dummy;
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                   CaretChangedReason::Updateposition));
+    EXPECT_CALL(check, Call("singletap updatecarets"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                  CaretChangedReason::Updateposition));
+    EXPECT_CALL(check, Call("longtap updatecarets"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                  CaretChangedReason::Visibilitychange));
+    EXPECT_CALL(check, Call("longtap scrollstart1"));
+
+    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
+      .WillOnce(Return(PositionChangedResult::Invisible));
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                  CaretChangedReason::Updateposition));
+    EXPECT_CALL(check, Call("longtap scrollend1"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                  CaretChangedReason::Visibilitychange));
+    EXPECT_CALL(check, Call("longtap scrollstart2"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                  CaretChangedReason::Updateposition));
+    EXPECT_CALL(check, Call("longtap scrollend2"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                  CaretChangedReason::Visibilitychange));
+    EXPECT_CALL(check, Call("longtap scrollstart3"));
+
+    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
+                  CaretChangedReason::Updateposition));
+    EXPECT_CALL(check, Call("longtap scrollend3"));
+  }
+
+  AutoRestore<bool> savePref(
+    MockAccessibleCaretManager::sCaretShownWhenLongTappingOnEmptyContent);
+  MockAccessibleCaretManager::sCaretShownWhenLongTappingOnEmptyContent = true;
+
+  // Simulate a single tap on an empty input.
+  mManager.FirstCaret().SetAppearance(Appearance::None);
+  mManager.UpdateCarets();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
+  check.Call("singletap updatecarets");
+
+  // Scroll the caret within the viewport.
+  mManager.OnScrollStart();
+  mManager.OnScrollEnd();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
+
+  // Simulate a long tap on an empty input.
+  mManager.FirstCaret().SetAppearance(Appearance::Normal);
+  mManager.UpdateCarets();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
+  check.Call("longtap updatecarets");
+
+  // Scroll the caret to be out of the viewport.
+  mManager.OnScrollStart();
+  check.Call("longtap scrollstart1");
+  mManager.OnScrollEnd();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
+  check.Call("longtap scrollend1");
+
+  // Scroll the caret into the viewport.
+  mManager.OnScrollStart();
+  check.Call("longtap scrollstart2");
+  mManager.OnScrollEnd();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
+  check.Call("longtap scrollend2");
+
+  // Scroll the caret within the viewport.
+  mManager.OnScrollStart();
+  check.Call("longtap scrollstart3");
+  mManager.OnScrollEnd();
+  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
+  check.Call("longtap scrollend3");
+}
+
 } // namespace mozilla
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -925,17 +925,20 @@ pref("toolkit.telemetry.unified", false)
 
 // Unified AccessibleCarets (touch-caret and selection-carets).
 #ifdef NIGHTLY_BUILD
 pref("layout.accessiblecaret.enabled", true);
 #else
 pref("layout.accessiblecaret.enabled", false);
 #endif
 
-// Android need persistent carets and actionbar. Turn off the caret timeout.
+// Android needs to show the caret when long tapping on an empty content.
+pref("layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content", true);
+
+// Android needs persistent carets and actionbar. Turn off the caret timeout.
 pref("layout.accessiblecaret.timeout_ms", 0);
 
 // Android generates long tap (mouse) events.
 pref("layout.accessiblecaret.use_long_tap_injector", false);
 
 // AccessibleCarets behaviour is extended to support Android specific
 // requirements during caret-drag, tapping into empty inputs, and to
 // hide carets while maintaining ActionBar visiblity during page scroll.
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4939,16 +4939,19 @@ pref("layout.accessiblecaret.enabled", f
 pref("layout.accessiblecaret.width", "34.0");
 pref("layout.accessiblecaret.height", "36.0");
 pref("layout.accessiblecaret.margin-left", "-18.5");
 pref("layout.accessiblecaret.bar.width", "2.0");
 
 // Show the selection bars at the two ends of the selection highlight.
 pref("layout.accessiblecaret.bar.enabled", true);
 
+// Show the caret when long tapping on an empty content.
+pref("layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content", false);
+
 // Timeout in milliseconds to hide the accessiblecaret under cursor mode while
 // no one touches it. Set the value to 0 to disable this feature.
 pref("layout.accessiblecaret.timeout_ms", 3000);
 
 // Simulate long tap to select words on the platforms where APZ is not enabled
 // or long tap events does not fired by APZ.
 pref("layout.accessiblecaret.use_long_tap_injector", true);