Bug 1246064 - Support long press to show AccessibleCaret on empty input for Fennec. r=roc
--- 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);