mirror of
https://github.com/roytam1/palemoon27.git
synced 2026-05-26 14:30:27 +00:00
4b8580917e
- some pref. cleanup (21e17660e7) - add some font names and aliases (cb38962246) - remove unused dom.max_child_script_run_time (d214b353d4) - align strange layout.css.scroll-snap.enabled overwrite (f2562a5cc1) - reshuffle some preferences, remove unused (41f586186b) - more reshuffle and cleanup of preferences (0208aa32a3) - Bug 1168891 Part 1 - Refine two functions related to caret positioning. r=mats (86d718d60e) - Bug 1168891 Part 2 - Allow one caret to be dragged across the other caret. r=mats (9276eb7728) - part of Bug 1252802 - Web page scrolls when dragging caret in editable, r=snorp (31dade8b77) - Bug 1235508 - Re-implement fast Phone number selection on long-press, r=TYLin (59b6371d17) - Bug 1249201 Part 1 - Add "scroll" reason to CaretStateChangedEvent. r=smaug (b92ff6cbfc) - Bug 1249201 Part 2 - Show carets continuously when panning or zooming. r=mats,sebastian (ca5c51c479) - Bug 1245246: Add null check for mDocViewerPrint in nsPrintEngine::FirePrintingErrorEvent. r=roc (e9d5b49a3f) - Bug 1025267 - Make some -moz- prefixed pseudo-classes chrome-only. r=bz (238f7a85d4) - Bug 1259889 Part 1 - Add @supports -moz-bool-pref for internal-only style sheets. r=heycam (d716a7b884) - Bug 1237633 - Part 1: Percentages are not allowed in a <source-size-value>. r=jdm (52ccffbf86) - Bug 1081362 - Change nsStyleBasicShape pointer to an nsRefPtr, to avoid leak in unexpected case. r=dholbert (2a5cb8ffdd) - Bug 1264317 - Make the basic shape clip-path clipping use nsCSSValue::Array instead of nsCSSValueList. r=dholbert (7aaf39f2d7) - Bug 1247150 - Consistently use StyleSheetHandle::RefPtr* for outparams in nsLayoutStylesheetCache. r=dholbert (ddc85f29f8) - Bug 1251848: Check StyleSheetHandles for being null-flavored before derefing them, in assertions within nsLayoutStylesheetCache::InvalidateSheet. r=bholley (edb3924075) - Bug 1245260 - Add crashtest; r=hiro (6347e37750) - Bug 460209 - Add crashtest. (97b4786de2) - Bug 474377 - Add crashtest. (516b4e8164) - Bug 1264396 - Don't allow animation of 'display' property; r=heycam (6e94bcb26a) - missing bit of 759568 - Part 1 (fc954f075b) - part of Bug 1037483 replace microdata with microformats (4ff01e11d6) - Bug 1245334 - Make PromiseMessage.jsm ids more meaningful. r=baku (913ac1b9a5) - Bug 1094201 - Implement an Integration.jsm module for low-overhead registration of overrides. r=mak (9982624b90) - Bug 1167663 - Mark nsCSSKeyframeStyleDeclaration/nsCSSPageStyleDeclaration::mRule as MOZ_NON_OWNING_REF. r=dbaron (6d4e9751a1) - Bug 1244992 - Avoid double-counting in various refcounted types related to nsCSSValue. r=heycam. (c830949dd9) - Bug 1262646 - Change the outparams passed to nsStyleUtil::AppendEscapedCSSString from nsString to nsAutoString. r=dholbert (2b0caadf9d) - Bug 1247336 - De-dupe changes in ActiveLayerTracker before treating property as animated. r=roc (c44ed5aee6) - space fix (5e79d245ea) - Bug 1266288 - Track changes to all margin properties for scroll-linked effects. r=mstange (fed6994e4d) - Bug 1259641 - Do not force reflow for all tabs when size mode changed. r=smaug (70847cc6d2) - Bug 1261265 - Fix nsStyleContext::MoveTo flag assertions to allow mismatch on parents if bit is set on child. r=dholbert (3e6b08372e) - Bug 1264837 Part 43 - Remove SVGFEUnstyledLeafFrameBase. r=dholbert (bb55feda77) - Remove mention of old SVG text pref in comment; no bug. (DONTBUILD) (3a618aca18) - Bug 752638, part 1 - Move SVGTextFrame::SetupContextPaint to nsSVGUtils. r=heycam (c125c2903f) - Bug 1258843 - Don't build SVG display items if their visibility is hidden. r=dholbert (150c3b0059) - Bug 1258650. Properly use aExtraMasksTransform when combining masks. r=Bas,a=kwierso (ba5ea1928b) - Bug 1263789 - Stop nsSVGMaskFrameNEON.h from polluting the global namespace. r=dholbert (e2c8544d35) - Bug 1162418 - Try to find a suitable non-zero dimension to use when containing block's inline-size depends on an SVG element which is specified as a percentage of its container. r=jwatt (3eab79c8a4) - Bug 1250143. Account for border/padding on outer <svg> elements in GeometryUtils. r=mats (f307820b75) - Bug 1243623. Don't skip unregistering a table part if we have a split table. r=mats (35bb0821c1) - Bug 1203417. Propagate error result from PaintTableFrame. r=seth (866e47b3e4) - Bug 1209780. Propagate the use of MOZ_MUST_USE DrawResult in nsTablePainter. r=seth (851618d06c) - var-const (29d5e9f859) - Bug 1209780. Propagate the use of MOZ_MUST_USE DrawResult in nsTreeBodyFrame::PaintText. r=seth (1ce563ea18) - Bug 1203626 - remove the unused argument from nsTreeBodyFrame::GetTwistyRect. r=mattwoodrow (03293f52b5) - Bug 1218041, part 1: Give nsTreeBodyFrame::PaintImage a fallback codepath for painting SVG images with no explicit height or width. r=seth (b6fd3a39f7) - Bug 1218041, part 2: add reftests for <treecell> SVG-image rendering. (no review) (90231e0bfa) - Bug 1224736: When image size lookup fails in nsTreeBodyFrame::PaintImage, only fall back to use the full destRect if we've got a VectorImage. r=tn (dd7d7667ca) - Bug 1156108 - Make nsTreeColumns::mFirstColumn an nsRefPtr; r=roc (f6888480bc) - Bug 1255069 - use UniquePtr for storage in nsTreeContentView; r=dholbert (598256735f) - Bug 1181560 - ensure previous menus get closed when opening new ones, r=Enn (2c88f3452a) - Bug 1192655 - Make menubar not react to events when it is not visible. r=enn (2bbcbc81a2) - Bug 1197913 - Keep the last hovered item highlighted after moving the cursor outside the <select> drop-down list on Windows. r=neil (abd3240473) - Bug 1228029 - Fix the usage of gtest assertion macros in TestJobScheduler.cpp. r=kats (0fcc9aa6fe) - Bug 1244234 - Simplify joining jobs with the gfx job scheduler. r=jrmuizel (f4b6bbf418) - Bug 1239288 - Add a shutdown test to the gfx job scheduler. r=jrmuizel (fd2432d108) - Bug 1239288 - Fix a race in the win32 job scheduler's shutdown. r=jrmuizel (4e509b4bf3) - Bug 1241161 - make Matrix4x4::ProjectTo2D normalize out perpective where possible. r=mattwoodrow (5a68e396a3) - bits of Bug 1135138 - Remove UNICODE from DEFINES (1eb51a0a79) - Bug 1249640: Part 4 Android to use new blocking. r=snorp (855e5c0dda) - Bug 1234875 - Remove alwaysAcceptSessionCookies pref. r=mak (8bed323449) - Bug 1247912 - convert left side expression to int64_t when assigning to mCookiesLifetimeSec in order to avoid overflow. r=jdm (0cedb68c83) - code and comment style (9215d74a8f) - code and comment style (1d4cda31af) - Bug 1219928 - Skip misspelled words in style blocks. r=enndeakin. (91dd0bcedf) - Bug 1236968 - autodial telemetry r=mayhemer (3844b9c19e) - Bug 1254310 - Add a hidden pref to temporarily disable Safe Browsing on given hostnames. r=gcp (4955fc88f8) - Bug 772528 - Remove nsFileInputStream::Seek() from nsPartialFileInputStream::Init(). r=baku (15db900fb5) - Bug 1150921 - Add telemetry for response codes to SafeBrowsing requests. r=francois f=bsmedberg (215d50e4ad) - Bug 1164518 - Better logging of completions. r=gcp (95b4fe3731) - Bug 1172688 - Add telemetry for when gethash calls timeout. r=francois, r=bsmedberg (b94a2b38a7) - Bug 1266184 - Implement nsIMIMEInputStream.data getter. r=mcmanus (8c9159c030) - Bug 1239955 - Let DNSService rely on IOService::Offline, r=bagder (336f161d21) - Bug 1260407 - added logging for proxy/pac to aid debugging, r=mcmanus (a179275ca6) - Bug 1259089 - Set TCP socket to non-blocking in sts again, just to be sure. r=mcmanus (bf0656bf07) - Bug 1256473 - Cast values to avoid C4838 on VS2015; r=mayhemer (d4b138dba8) - Bug 1260764 - Creation of PollableEvent needs a lock r=dragana a=kwierso (01c9d5e477) - Bug 652186 - Implement URL Standard's backslash replacement r=mcmanus (6485fa7e8c) - Bug 1042347 - %2e entered in URL bar not normalized leading to denormalized request r=mcmanus (3fc1ff92cd) - Bug 377052 - nsBaseURLParser::ParseURL doesn't handle spaces embedded in the scheme properly r=mcmanus (1f54055b9d) - fix editor format (444d6a62c4) - Bug 1154124 - Prevent recursion when calling HTTP cache entry's callbacks. r=michal (7bdfbf603d) - Bug 1247644 - Don't do any I/O on doomed and unused HTTP cache entries, r=michal (7668d29a36)
1395 lines
41 KiB
C++
1395 lines
41 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* 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/. */
|
|
|
|
#include "AccessibleCaretManager.h"
|
|
|
|
#include "AccessibleCaret.h"
|
|
#include "AccessibleCaretEventHub.h"
|
|
#include "AccessibleCaretLogger.h"
|
|
#include "mozilla/AsyncEventDispatcher.h"
|
|
#include "mozilla/dom/Element.h"
|
|
#include "mozilla/dom/Selection.h"
|
|
#include "mozilla/dom/TreeWalker.h"
|
|
#include "mozilla/IMEStateManager.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "nsCaret.h"
|
|
#include "nsContainerFrame.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsFocusManager.h"
|
|
#include "nsFrame.h"
|
|
#include "nsFrameSelection.h"
|
|
#include "nsGenericHTMLElement.h"
|
|
#include "nsIHapticFeedback.h"
|
|
#ifdef MOZ_WIDGET_ANDROID
|
|
#include "nsWindow.h"
|
|
#endif
|
|
|
|
namespace mozilla {
|
|
|
|
#undef AC_LOG
|
|
#define AC_LOG(message, ...) \
|
|
AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
|
|
|
|
#undef AC_LOGV
|
|
#define AC_LOGV(message, ...) \
|
|
AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
|
|
|
|
using namespace dom;
|
|
using Appearance = AccessibleCaret::Appearance;
|
|
using PositionChangedResult = AccessibleCaret::PositionChangedResult;
|
|
|
|
#define AC_PROCESS_ENUM_TO_STREAM(e) case(e): aStream << #e; break;
|
|
std::ostream&
|
|
operator<<(std::ostream& aStream,
|
|
const AccessibleCaretManager::CaretMode& aCaretMode)
|
|
{
|
|
using CaretMode = AccessibleCaretManager::CaretMode;
|
|
switch (aCaretMode) {
|
|
AC_PROCESS_ENUM_TO_STREAM(CaretMode::None);
|
|
AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor);
|
|
AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection);
|
|
}
|
|
return aStream;
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& aStream,
|
|
const AccessibleCaretManager::UpdateCaretsHint& aHint)
|
|
{
|
|
using UpdateCaretsHint = AccessibleCaretManager::UpdateCaretsHint;
|
|
switch (aHint) {
|
|
AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default);
|
|
AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance);
|
|
}
|
|
return aStream;
|
|
}
|
|
#undef AC_PROCESS_ENUM_TO_STREAM
|
|
|
|
/*static*/ bool
|
|
AccessibleCaretManager::sSelectionBarEnabled = false;
|
|
/*static*/ bool
|
|
AccessibleCaretManager::sCaretShownWhenLongTappingOnEmptyContent = false;
|
|
/*static*/ bool
|
|
AccessibleCaretManager::sCaretsAlwaysTilt = false;
|
|
/*static*/ bool
|
|
AccessibleCaretManager::sCaretsAlwaysShowWhenScrolling = true;
|
|
/*static*/ bool
|
|
AccessibleCaretManager::sCaretsScriptUpdates = false;
|
|
/*static*/ bool
|
|
AccessibleCaretManager::sCaretsAllowDraggingAcrossOtherCaret = true;
|
|
/*static*/ bool
|
|
AccessibleCaretManager::sHapticFeedback = false;
|
|
/*static*/ bool
|
|
AccessibleCaretManager::sExtendSelectionForPhoneNumber = false;
|
|
|
|
AccessibleCaretManager::AccessibleCaretManager(nsIPresShell* aPresShell)
|
|
: mPresShell(aPresShell)
|
|
{
|
|
if (!mPresShell) {
|
|
return;
|
|
}
|
|
|
|
mFirstCaret = MakeUnique<AccessibleCaret>(mPresShell);
|
|
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(&sCaretsAlwaysTilt,
|
|
"layout.accessiblecaret.always_tilt");
|
|
Preferences::AddBoolVarCache(&sCaretsAlwaysShowWhenScrolling,
|
|
"layout.accessiblecaret.always_show_when_scrolling", true);
|
|
Preferences::AddBoolVarCache(&sCaretsScriptUpdates,
|
|
"layout.accessiblecaret.allow_script_change_updates");
|
|
Preferences::AddBoolVarCache(&sCaretsAllowDraggingAcrossOtherCaret,
|
|
"layout.accessiblecaret.allow_dragging_across_other_caret", true);
|
|
Preferences::AddBoolVarCache(&sHapticFeedback,
|
|
"layout.accessiblecaret.hapticfeedback");
|
|
Preferences::AddBoolVarCache(&sExtendSelectionForPhoneNumber,
|
|
"layout.accessiblecaret.extend_selection_for_phone_number");
|
|
addedPrefs = true;
|
|
}
|
|
}
|
|
|
|
AccessibleCaretManager::~AccessibleCaretManager()
|
|
{
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::Terminate()
|
|
{
|
|
CancelCaretTimeoutTimer();
|
|
mCaretTimeoutTimer = nullptr;
|
|
mFirstCaret = nullptr;
|
|
mSecondCaret = nullptr;
|
|
mActiveCaret = nullptr;
|
|
mPresShell = nullptr;
|
|
}
|
|
|
|
nsresult
|
|
AccessibleCaretManager::OnSelectionChanged(nsIDOMDocument* aDoc,
|
|
nsISelection* aSel, int16_t aReason)
|
|
{
|
|
Selection* selection = GetSelection();
|
|
AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__,
|
|
aSel, selection, aReason);
|
|
if (aSel != selection) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// eSetSelection events from the Fennec widget IME can be generated
|
|
// by autoSuggest, autoCorrect, and nsCaret position changes.
|
|
if (aReason & nsISelectionListener::IME_REASON) {
|
|
if (GetCaretMode() == CaretMode::Cursor) {
|
|
// Caret position changes need us to open/update,
|
|
// or hide the AccessibleCaret.
|
|
FlushLayout();
|
|
UpdateCarets();
|
|
} else {
|
|
// Ignore transient autoSuggest selection styling,
|
|
// or autoCorrect text updates.
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
// Move the cursor by Javascript / or unknown internal.
|
|
if (aReason == nsISelectionListener::NO_REASON) {
|
|
// Update visible carets, if javascript changes are allowed.
|
|
if (sCaretsScriptUpdates &&
|
|
(mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible())) {
|
|
UpdateCarets();
|
|
return NS_OK;
|
|
}
|
|
// Default for NO_REASON is to make hidden.
|
|
HideCarets();
|
|
return NS_OK;
|
|
}
|
|
|
|
// Move cursor by keyboard.
|
|
if (aReason & nsISelectionListener::KEYPRESS_REASON) {
|
|
HideCarets();
|
|
return NS_OK;
|
|
}
|
|
|
|
// OnBlur() might be called between mouse down and mouse up, so we hide carets
|
|
// upon mouse down anyway, and update carets upon mouse up.
|
|
if (aReason & nsISelectionListener::MOUSEDOWN_REASON) {
|
|
HideCarets();
|
|
return NS_OK;
|
|
}
|
|
|
|
// Range will collapse after cutting or copying text.
|
|
if (aReason & (nsISelectionListener::COLLAPSETOSTART_REASON |
|
|
nsISelectionListener::COLLAPSETOEND_REASON)) {
|
|
HideCarets();
|
|
return NS_OK;
|
|
}
|
|
|
|
UpdateCarets();
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::HideCarets()
|
|
{
|
|
if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
|
|
AC_LOG("%s", __FUNCTION__);
|
|
mFirstCaret->SetAppearance(Appearance::None);
|
|
mSecondCaret->SetAppearance(Appearance::None);
|
|
DispatchCaretStateChangedEvent(CaretChangedReason::Visibilitychange);
|
|
CancelCaretTimeoutTimer();
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::UpdateCarets(UpdateCaretsHint aHint)
|
|
{
|
|
FlushLayout();
|
|
if (IsTerminated()) {
|
|
return;
|
|
}
|
|
|
|
mLastUpdateCaretMode = GetCaretMode();
|
|
|
|
switch (mLastUpdateCaretMode) {
|
|
case CaretMode::None:
|
|
HideCarets();
|
|
break;
|
|
case CaretMode::Cursor:
|
|
UpdateCaretsForCursorMode(aHint);
|
|
break;
|
|
case CaretMode::Selection:
|
|
UpdateCaretsForSelectionMode(aHint);
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool
|
|
AccessibleCaretManager::IsCaretDisplayableInCursorMode(nsIFrame** aOutFrame,
|
|
int32_t* aOutOffset) const
|
|
{
|
|
RefPtr<nsCaret> caret = mPresShell->GetCaret();
|
|
if (!caret || !caret->IsVisible()) {
|
|
return false;
|
|
}
|
|
|
|
int32_t offset = 0;
|
|
nsIFrame* frame = nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset);
|
|
|
|
if (!frame) {
|
|
return false;
|
|
}
|
|
|
|
if (!GetEditingHostForFrame(frame)) {
|
|
return false;
|
|
}
|
|
|
|
if (aOutFrame) {
|
|
*aOutFrame = frame;
|
|
}
|
|
|
|
if (aOutOffset) {
|
|
*aOutOffset = offset;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
AccessibleCaretManager::HasNonEmptyTextContent(nsINode* aNode) const
|
|
{
|
|
return nsContentUtils::HasNonEmptyTextContent(
|
|
aNode, nsContentUtils::eRecurseIntoChildren);
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::UpdateCaretsForCursorMode(UpdateCaretsHint aHint)
|
|
{
|
|
AC_LOG("%s: selection: %p", __FUNCTION__, GetSelection());
|
|
|
|
int32_t offset = 0;
|
|
nsIFrame* frame = nullptr;
|
|
if (!IsCaretDisplayableInCursorMode(&frame, &offset)) {
|
|
HideCarets();
|
|
return;
|
|
}
|
|
|
|
bool oldSecondCaretVisible = mSecondCaret->IsLogicallyVisible();
|
|
PositionChangedResult result = mFirstCaret->SetPosition(frame, offset);
|
|
|
|
switch (result) {
|
|
case PositionChangedResult::NotChanged:
|
|
// 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 preserve the appearance of the caret set by the
|
|
// caller.
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case PositionChangedResult::Invisible:
|
|
mFirstCaret->SetAppearance(Appearance::NormalNotShown);
|
|
break;
|
|
}
|
|
|
|
mFirstCaret->SetSelectionBarEnabled(false);
|
|
mSecondCaret->SetAppearance(Appearance::None);
|
|
|
|
LaunchCaretTimeoutTimer();
|
|
|
|
if ((result != PositionChangedResult::NotChanged || oldSecondCaretVisible) &&
|
|
!mActiveCaret) {
|
|
DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::UpdateCaretsForSelectionMode(UpdateCaretsHint aHint)
|
|
{
|
|
AC_LOG("%s: selection: %p", __FUNCTION__, GetSelection());
|
|
|
|
int32_t startOffset = 0;
|
|
nsIFrame* startFrame =
|
|
GetFrameForFirstRangeStartOrLastRangeEnd(eDirNext, &startOffset);
|
|
|
|
int32_t endOffset = 0;
|
|
nsIFrame* endFrame =
|
|
GetFrameForFirstRangeStartOrLastRangeEnd(eDirPrevious, &endOffset);
|
|
|
|
if (!CompareTreePosition(startFrame, endFrame)) {
|
|
// XXX: Do we really have to hide carets if this condition isn't satisfied?
|
|
HideCarets();
|
|
return;
|
|
}
|
|
|
|
auto updateSingleCaret = [aHint](AccessibleCaret* aCaret, nsIFrame* aFrame,
|
|
int32_t aOffset) -> PositionChangedResult
|
|
{
|
|
PositionChangedResult result = aCaret->SetPosition(aFrame, aOffset);
|
|
aCaret->SetSelectionBarEnabled(sSelectionBarEnabled);
|
|
|
|
switch (result) {
|
|
case PositionChangedResult::NotChanged:
|
|
// Do nothing
|
|
break;
|
|
|
|
case PositionChangedResult::Changed:
|
|
switch (aHint) {
|
|
case UpdateCaretsHint::Default:
|
|
aCaret->SetAppearance(Appearance::Normal);
|
|
break;
|
|
|
|
case UpdateCaretsHint::RespectOldAppearance:
|
|
// Do nothing to preserve the appearance of the caret set by the
|
|
// caller.
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case PositionChangedResult::Invisible:
|
|
aCaret->SetAppearance(Appearance::NormalNotShown);
|
|
break;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
PositionChangedResult firstCaretResult =
|
|
updateSingleCaret(mFirstCaret.get(), startFrame, startOffset);
|
|
PositionChangedResult secondCaretResult =
|
|
updateSingleCaret(mSecondCaret.get(), endFrame, endOffset);
|
|
|
|
if (firstCaretResult == PositionChangedResult::Changed ||
|
|
secondCaretResult == PositionChangedResult::Changed) {
|
|
// Flush layout to make the carets intersection correct.
|
|
FlushLayout();
|
|
if (IsTerminated()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (aHint == UpdateCaretsHint::Default) {
|
|
// Only check for tilt carets with default update hint. Otherwise we might
|
|
// override the appearance set by the caller.
|
|
if (sCaretsAlwaysTilt) {
|
|
UpdateCaretsForAlwaysTilt(startFrame, endFrame);
|
|
} else {
|
|
UpdateCaretsForOverlappingTilt();
|
|
}
|
|
}
|
|
|
|
if (!mActiveCaret) {
|
|
DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::UpdateCaretsForOverlappingTilt()
|
|
{
|
|
if (mFirstCaret->IsVisuallyVisible() && mSecondCaret->IsVisuallyVisible()) {
|
|
if (mFirstCaret->Intersects(*mSecondCaret)) {
|
|
if (mFirstCaret->LogicalPosition().x <=
|
|
mSecondCaret->LogicalPosition().x) {
|
|
mFirstCaret->SetAppearance(Appearance::Left);
|
|
mSecondCaret->SetAppearance(Appearance::Right);
|
|
} else {
|
|
mFirstCaret->SetAppearance(Appearance::Right);
|
|
mSecondCaret->SetAppearance(Appearance::Left);
|
|
}
|
|
} else {
|
|
mFirstCaret->SetAppearance(Appearance::Normal);
|
|
mSecondCaret->SetAppearance(Appearance::Normal);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::UpdateCaretsForAlwaysTilt(nsIFrame* aStartFrame,
|
|
nsIFrame* aEndFrame)
|
|
{
|
|
if (mFirstCaret->IsVisuallyVisible()) {
|
|
auto startFrameWritingMode = aStartFrame->GetWritingMode();
|
|
mFirstCaret->SetAppearance(startFrameWritingMode.IsBidiLTR() ?
|
|
Appearance::Left : Appearance::Right);
|
|
}
|
|
if (mSecondCaret->IsVisuallyVisible()) {
|
|
auto endFrameWritingMode = aEndFrame->GetWritingMode();
|
|
mSecondCaret->SetAppearance(endFrameWritingMode.IsBidiLTR() ?
|
|
Appearance::Right : Appearance::Left);
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::ProvideHapticFeedback()
|
|
{
|
|
if (sHapticFeedback) {
|
|
nsCOMPtr<nsIHapticFeedback> haptic =
|
|
do_GetService("@mozilla.org/widget/hapticfeedback;1");
|
|
haptic->PerformSimpleAction(haptic->LongPress);
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
AccessibleCaretManager::PressCaret(const nsPoint& aPoint)
|
|
{
|
|
nsresult rv = NS_ERROR_FAILURE;
|
|
|
|
if (mFirstCaret->Contains(aPoint)) {
|
|
mActiveCaret = mFirstCaret.get();
|
|
SetSelectionDirection(eDirPrevious);
|
|
} else if (mSecondCaret->Contains(aPoint)) {
|
|
mActiveCaret = mSecondCaret.get();
|
|
SetSelectionDirection(eDirNext);
|
|
}
|
|
|
|
if (mActiveCaret) {
|
|
mOffsetYToCaretLogicalPosition =
|
|
mActiveCaret->LogicalPosition().y - aPoint.y;
|
|
SetSelectionDragState(true);
|
|
DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret);
|
|
CancelCaretTimeoutTimer();
|
|
rv = NS_OK;
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
nsresult
|
|
AccessibleCaretManager::DragCaret(const nsPoint& aPoint)
|
|
{
|
|
MOZ_ASSERT(mActiveCaret);
|
|
MOZ_ASSERT(GetCaretMode() != CaretMode::None);
|
|
|
|
nsPoint point(aPoint.x, aPoint.y + mOffsetYToCaretLogicalPosition);
|
|
DragCaretInternal(point);
|
|
UpdateCarets();
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
AccessibleCaretManager::ReleaseCaret()
|
|
{
|
|
MOZ_ASSERT(mActiveCaret);
|
|
|
|
mActiveCaret = nullptr;
|
|
SetSelectionDragState(false);
|
|
DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret);
|
|
LaunchCaretTimeoutTimer();
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
AccessibleCaretManager::TapCaret(const nsPoint& aPoint)
|
|
{
|
|
MOZ_ASSERT(GetCaretMode() != CaretMode::None);
|
|
|
|
nsresult rv = NS_ERROR_FAILURE;
|
|
|
|
if (GetCaretMode() == CaretMode::Cursor) {
|
|
DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret);
|
|
rv = NS_OK;
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
nsresult
|
|
AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint)
|
|
{
|
|
auto UpdateCaretsWithHapticFeedback = [this] {
|
|
UpdateCarets();
|
|
ProvideHapticFeedback();
|
|
};
|
|
|
|
if (!mPresShell) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
nsIFrame* rootFrame = mPresShell->GetRootFrame();
|
|
if (!rootFrame) {
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
// Find the frame under point.
|
|
nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(rootFrame, aPoint,
|
|
nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
|
|
if (!ptFrame) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
nsIFrame* focusableFrame = GetFocusableFrame(ptFrame);
|
|
|
|
#ifdef DEBUG_FRAME_DUMP
|
|
AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__, ptFrame->ListTag().get(),
|
|
aPoint.x, aPoint.y);
|
|
AC_LOG("%s: Found %s focusable", __FUNCTION__,
|
|
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;
|
|
ptFrame->IsSelectable(&selectable, nullptr);
|
|
|
|
#ifdef DEBUG_FRAME_DUMP
|
|
AC_LOG("%s: %s %s selectable.", __FUNCTION__, ptFrame->ListTag().get(),
|
|
selectable ? "is" : "is NOT");
|
|
#endif
|
|
|
|
if (!selectable) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// Commit the composition string of the old editable focus element (if there
|
|
// is any) before changing the focus.
|
|
IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION,
|
|
mPresShell->GetPresContext());
|
|
|
|
// ptFrame is selectable. Now change the focus.
|
|
ChangeFocusToOrClearOldFocus(focusableFrame);
|
|
|
|
if (GetCaretMode() == CaretMode::Selection &&
|
|
!mFirstCaret->IsLogicallyVisible() && !mSecondCaret->IsLogicallyVisible()) {
|
|
// We have a selection while both carets have Appearance::None because of
|
|
// previous operations like blur event. Just update carets on the selection
|
|
// without selecting a new word.
|
|
AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__);
|
|
UpdateCaretsWithHapticFeedback();
|
|
return NS_OK;
|
|
}
|
|
|
|
// Then try select a word under point.
|
|
nsPoint ptInFrame = aPoint;
|
|
nsLayoutUtils::TransformPoint(rootFrame, ptFrame, ptInFrame);
|
|
|
|
nsresult rv = SelectWord(ptFrame, ptInFrame);
|
|
UpdateCaretsWithHapticFeedback();
|
|
|
|
return rv;
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::OnScrollStart()
|
|
{
|
|
AC_LOG("%s", __FUNCTION__);
|
|
|
|
if (!sCaretsAlwaysShowWhenScrolling) {
|
|
// Backup the appearance so that we can restore them after the scrolling
|
|
// ends.
|
|
mFirstCaretAppearanceOnScrollStart = mFirstCaret->GetAppearance();
|
|
mSecondCaretAppearanceOnScrollStart = mSecondCaret->GetAppearance();
|
|
HideCarets();
|
|
return;
|
|
}
|
|
|
|
if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
|
|
// Dispatch the event only if one of the carets is logically visible like in
|
|
// HideCarets().
|
|
DispatchCaretStateChangedEvent(CaretChangedReason::Scroll);
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::OnScrollEnd()
|
|
{
|
|
if (mLastUpdateCaretMode != GetCaretMode()) {
|
|
return;
|
|
}
|
|
|
|
if (!sCaretsAlwaysShowWhenScrolling) {
|
|
// Restore the appearance which is saved before the scrolling is started.
|
|
mFirstCaret->SetAppearance(mFirstCaretAppearanceOnScrollStart);
|
|
mSecondCaret->SetAppearance(mSecondCaretAppearanceOnScrollStart);
|
|
}
|
|
|
|
if (GetCaretMode() == CaretMode::Cursor) {
|
|
if (!mFirstCaret->IsLogicallyVisible()) {
|
|
// If the caret is hidden (Appearance::None) due to timeout or blur, no
|
|
// need to update it.
|
|
return;
|
|
}
|
|
}
|
|
|
|
AC_LOG("%s: UpdateCarets()", __FUNCTION__);
|
|
UpdateCarets();
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::OnScrollPositionChanged()
|
|
{
|
|
if (mLastUpdateCaretMode != GetCaretMode()) {
|
|
return;
|
|
}
|
|
|
|
if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
|
|
AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
|
|
UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::OnReflow()
|
|
{
|
|
if (mLastUpdateCaretMode != GetCaretMode()) {
|
|
return;
|
|
}
|
|
|
|
if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
|
|
AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
|
|
UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::OnBlur()
|
|
{
|
|
AC_LOG("%s: HideCarets()", __FUNCTION__);
|
|
HideCarets();
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::OnKeyboardEvent()
|
|
{
|
|
if (GetCaretMode() == CaretMode::Cursor) {
|
|
AC_LOG("%s: HideCarets()", __FUNCTION__);
|
|
HideCarets();
|
|
}
|
|
}
|
|
|
|
Selection*
|
|
AccessibleCaretManager::GetSelection() const
|
|
{
|
|
RefPtr<nsFrameSelection> fs = GetFrameSelection();
|
|
if (!fs) {
|
|
return nullptr;
|
|
}
|
|
return fs->GetSelection(nsISelectionController::SELECTION_NORMAL);
|
|
}
|
|
|
|
already_AddRefed<nsFrameSelection>
|
|
AccessibleCaretManager::GetFrameSelection() const
|
|
{
|
|
if (!mPresShell) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsFocusManager* fm = nsFocusManager::GetFocusManager();
|
|
MOZ_ASSERT(fm);
|
|
|
|
nsIContent* focusedContent = fm->GetFocusedContent();
|
|
if (focusedContent) {
|
|
nsIFrame* focusFrame = focusedContent->GetPrimaryFrame();
|
|
if (!focusFrame) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Prevent us from touching the nsFrameSelection associated with other
|
|
// PresShell.
|
|
RefPtr<nsFrameSelection> fs = focusFrame->GetFrameSelection();
|
|
if (!fs || fs->GetShell() != mPresShell) {
|
|
return nullptr;
|
|
}
|
|
|
|
return fs.forget();
|
|
} else {
|
|
// For non-editable content
|
|
return mPresShell->FrameSelection();
|
|
}
|
|
}
|
|
|
|
Element*
|
|
AccessibleCaretManager::GetEditingHostForFrame(nsIFrame* aFrame) const
|
|
{
|
|
if (!aFrame) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto content = aFrame->GetContent();
|
|
if (!content) {
|
|
return nullptr;
|
|
}
|
|
|
|
return content->GetEditingHost();
|
|
}
|
|
|
|
|
|
AccessibleCaretManager::CaretMode
|
|
AccessibleCaretManager::GetCaretMode() const
|
|
{
|
|
Selection* selection = GetSelection();
|
|
if (!selection) {
|
|
return CaretMode::None;
|
|
}
|
|
|
|
uint32_t rangeCount = selection->RangeCount();
|
|
if (rangeCount <= 0) {
|
|
return CaretMode::None;
|
|
}
|
|
|
|
if (selection->IsCollapsed()) {
|
|
return CaretMode::Cursor;
|
|
}
|
|
|
|
return CaretMode::Selection;
|
|
}
|
|
|
|
nsIFrame*
|
|
AccessibleCaretManager::GetFocusableFrame(nsIFrame* aFrame) const
|
|
{
|
|
// This implementation is similar to EventStateManager::PostHandleEvent().
|
|
// Look for the nearest enclosing focusable frame.
|
|
nsIFrame* focusableFrame = aFrame;
|
|
while (focusableFrame) {
|
|
if (focusableFrame->IsFocusable(nullptr, true)) {
|
|
break;
|
|
}
|
|
focusableFrame = focusableFrame->GetParent();
|
|
}
|
|
return focusableFrame;
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::ChangeFocusToOrClearOldFocus(nsIFrame* aFrame) const
|
|
{
|
|
nsFocusManager* fm = nsFocusManager::GetFocusManager();
|
|
MOZ_ASSERT(fm);
|
|
|
|
if (aFrame) {
|
|
nsIContent* focusableContent = aFrame->GetContent();
|
|
MOZ_ASSERT(focusableContent, "Focusable frame must have content!");
|
|
nsCOMPtr<nsIDOMElement> focusableElement = do_QueryInterface(focusableContent);
|
|
fm->SetFocus(focusableElement, nsIFocusManager::FLAG_BYMOUSE);
|
|
} else {
|
|
nsIDOMWindow* win = mPresShell->GetDocument()->GetWindow();
|
|
if (win) {
|
|
fm->ClearFocus(win);
|
|
fm->SetFocusedWindow(win);
|
|
}
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
AccessibleCaretManager::SelectWord(nsIFrame* aFrame, const nsPoint& aPoint) const
|
|
{
|
|
SetSelectionDragState(true);
|
|
nsFrame* frame = static_cast<nsFrame*>(aFrame);
|
|
nsresult rs = frame->SelectByTypeAtPoint(mPresShell->GetPresContext(), aPoint,
|
|
eSelectWord, eSelectWord, 0);
|
|
|
|
SetSelectionDragState(false);
|
|
ClearMaintainedSelection();
|
|
|
|
// Smart-select phone numbers if possible.
|
|
if (sExtendSelectionForPhoneNumber) {
|
|
SelectMoreIfPhoneNumber();
|
|
}
|
|
|
|
return rs;
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::SetSelectionDragState(bool aState) const
|
|
{
|
|
RefPtr<nsFrameSelection> fs = GetFrameSelection();
|
|
if (fs) {
|
|
fs->SetDragState(aState);
|
|
}
|
|
|
|
// Pin Fennecs DynamicToolbarAnimator in place before/after dragging,
|
|
// to avoid co-incident screen scrolling.
|
|
#ifdef MOZ_WIDGET_ANDROID
|
|
nsIDocument* doc = mPresShell->GetDocument();
|
|
MOZ_ASSERT(doc);
|
|
nsIWidget* widget = nsContentUtils::WidgetForDocument(doc);
|
|
static_cast<nsWindow*>(widget)->SetSelectionDragState(aState);
|
|
#endif
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::SelectMoreIfPhoneNumber() const
|
|
{
|
|
SetSelectionDirection(eDirNext);
|
|
ExtendPhoneNumberSelection(NS_LITERAL_STRING("forward"));
|
|
|
|
SetSelectionDirection(eDirPrevious);
|
|
ExtendPhoneNumberSelection(NS_LITERAL_STRING("backward"));
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::ExtendPhoneNumberSelection(const nsAString& aDirection) const
|
|
{
|
|
nsIDocument* doc = mPresShell->GetDocument();
|
|
|
|
// Extend the phone number selection until we find a boundary.
|
|
Selection* selection = GetSelection();
|
|
|
|
while (selection) {
|
|
// Save current Focus position, and extend the selection one char.
|
|
nsINode* focusNode = selection->GetFocusNode();
|
|
uint32_t focusOffset = selection->FocusOffset();
|
|
selection->Modify(NS_LITERAL_STRING("extend"),
|
|
aDirection,
|
|
NS_LITERAL_STRING("character"));
|
|
|
|
// If the selection didn't change, (can't extend further), we're done.
|
|
if (selection->GetFocusNode() == focusNode &&
|
|
selection->FocusOffset() == focusOffset) {
|
|
return;
|
|
}
|
|
|
|
// If the changed selection isn't a valid phone number, we're done.
|
|
nsAutoString selectedText;
|
|
selection->Stringify(selectedText);
|
|
nsAutoString phoneRegex(NS_LITERAL_STRING("(^\\+)?[0-9\\s,\\-.()*#pw]{1,30}$"));
|
|
|
|
if (!nsContentUtils::IsPatternMatching(selectedText, phoneRegex, doc)) {
|
|
// Backout the undesired selection extend, (collapse to original
|
|
// Anchor, extend to original Focus), before exit.
|
|
selection->Collapse(selection->GetAnchorNode(), selection->AnchorOffset());
|
|
selection->Extend(focusNode, focusOffset);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const
|
|
{
|
|
Selection* selection = GetSelection();
|
|
if (selection) {
|
|
selection->AdjustAnchorFocusForMultiRange(aDir);
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::ClearMaintainedSelection() const
|
|
{
|
|
// Selection made by double-clicking for example will maintain the original
|
|
// word selection. We should clear it so that we can drag caret freely.
|
|
RefPtr<nsFrameSelection> fs = GetFrameSelection();
|
|
if (fs) {
|
|
fs->MaintainSelection(eSelectNoAmount);
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::FlushLayout() const
|
|
{
|
|
if (mPresShell) {
|
|
mPresShell->FlushPendingNotifications(Flush_Layout);
|
|
}
|
|
}
|
|
|
|
nsIFrame*
|
|
AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd(
|
|
nsDirection aDirection, int32_t* aOutOffset, nsINode** aOutNode,
|
|
int32_t* aOutNodeOffset) const
|
|
{
|
|
if (!mPresShell) {
|
|
return nullptr;
|
|
}
|
|
|
|
MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
|
|
|
|
nsRange* range = nullptr;
|
|
RefPtr<nsINode> startNode;
|
|
RefPtr<nsINode> endNode;
|
|
int32_t nodeOffset = 0;
|
|
CaretAssociationHint hint;
|
|
|
|
RefPtr<Selection> selection = GetSelection();
|
|
bool findInFirstRangeStart = aDirection == eDirNext;
|
|
|
|
if (findInFirstRangeStart) {
|
|
range = selection->GetRangeAt(0);
|
|
startNode = range->GetStartParent();
|
|
endNode = range->GetEndParent();
|
|
nodeOffset = range->StartOffset();
|
|
hint = CARET_ASSOCIATE_AFTER;
|
|
} else {
|
|
range = selection->GetRangeAt(selection->RangeCount() - 1);
|
|
startNode = range->GetEndParent();
|
|
endNode = range->GetStartParent();
|
|
nodeOffset = range->EndOffset();
|
|
hint = CARET_ASSOCIATE_BEFORE;
|
|
}
|
|
|
|
nsCOMPtr<nsIContent> startContent = do_QueryInterface(startNode);
|
|
RefPtr<nsFrameSelection> fs = GetFrameSelection();
|
|
nsIFrame* startFrame =
|
|
fs->GetFrameForNodeOffset(startContent, nodeOffset, hint, aOutOffset);
|
|
|
|
if (startFrame) {
|
|
if (aOutNode) {
|
|
*aOutNode = startNode.get();
|
|
}
|
|
if (aOutNodeOffset) {
|
|
*aOutNodeOffset = nodeOffset;
|
|
}
|
|
return startFrame;
|
|
}
|
|
|
|
ErrorResult err;
|
|
RefPtr<TreeWalker> walker = mPresShell->GetDocument()->CreateTreeWalker(
|
|
*startNode, nsIDOMNodeFilter::SHOW_ALL, nullptr, err);
|
|
|
|
if (!walker) {
|
|
return nullptr;
|
|
}
|
|
|
|
startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
|
|
while (!startFrame && startNode != endNode) {
|
|
startNode = findInFirstRangeStart ? walker->NextNode(err)
|
|
: walker->PreviousNode(err);
|
|
|
|
if (!startNode) {
|
|
break;
|
|
}
|
|
|
|
startContent = startNode->AsContent();
|
|
startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
|
|
}
|
|
return startFrame;
|
|
}
|
|
|
|
bool
|
|
AccessibleCaretManager::RestrictCaretDraggingOffsets(
|
|
nsIFrame::ContentOffsets& aOffsets)
|
|
{
|
|
if (!mPresShell) {
|
|
return false;
|
|
}
|
|
|
|
MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
|
|
|
|
nsDirection dir = mActiveCaret == mFirstCaret.get() ? eDirPrevious : eDirNext;
|
|
int32_t offset = 0;
|
|
nsINode* node = nullptr;
|
|
int32_t contentOffset = 0;
|
|
nsIFrame* frame =
|
|
GetFrameForFirstRangeStartOrLastRangeEnd(dir, &offset, &node, &contentOffset);
|
|
|
|
if (!frame) {
|
|
return false;
|
|
}
|
|
|
|
nsCOMPtr<nsIContent> content = do_QueryInterface(node);
|
|
|
|
// Compare the active caret's new position (aOffsets) to the inactive caret's
|
|
// position.
|
|
int32_t cmpToInactiveCaretPos =
|
|
nsContentUtils::ComparePoints(aOffsets.content, aOffsets.StartOffset(),
|
|
content, contentOffset);
|
|
|
|
// Move one character (in the direction of dir) from the inactive caret's
|
|
// position. This is the limit for the active caret's new position.
|
|
nsPeekOffsetStruct limit(eSelectCluster, dir, offset, nsPoint(0, 0), true, true,
|
|
false, false, false);
|
|
nsresult rv = frame->PeekOffset(&limit);
|
|
if (NS_FAILED(rv)) {
|
|
limit.mResultContent = content;
|
|
limit.mContentOffset = contentOffset;
|
|
}
|
|
|
|
// Compare the active caret's new position (aOffsets) to the limit.
|
|
int32_t cmpToLimit =
|
|
nsContentUtils::ComparePoints(aOffsets.content, aOffsets.StartOffset(),
|
|
limit.mResultContent, limit.mContentOffset);
|
|
|
|
auto SetOffsetsToLimit = [&aOffsets, &limit] () {
|
|
aOffsets.content = limit.mResultContent;
|
|
aOffsets.offset = limit.mContentOffset;
|
|
aOffsets.secondaryOffset = limit.mContentOffset;
|
|
};
|
|
|
|
if (!sCaretsAllowDraggingAcrossOtherCaret) {
|
|
if ((mActiveCaret == mFirstCaret.get() && cmpToLimit == 1) ||
|
|
(mActiveCaret == mSecondCaret.get() && cmpToLimit == -1)) {
|
|
// The active caret's position is past the limit, which we don't allow
|
|
// here. So set it to the limit, resulting in one character being
|
|
// selected.
|
|
SetOffsetsToLimit();
|
|
}
|
|
} else {
|
|
switch (cmpToInactiveCaretPos) {
|
|
case 0:
|
|
// The active caret's position is the same as the position of the
|
|
// inactive caret. So set it to the limit to prevent the selection from
|
|
// being collapsed, resulting in one character being selected.
|
|
SetOffsetsToLimit();
|
|
break;
|
|
case 1:
|
|
if (mActiveCaret == mFirstCaret.get()) {
|
|
// First caret was moved across the second caret. After making change
|
|
// to the selection, the user will drag the second caret.
|
|
mActiveCaret = mSecondCaret.get();
|
|
}
|
|
break;
|
|
case -1:
|
|
if (mActiveCaret == mSecondCaret.get()) {
|
|
// Second caret was moved across the first caret. After making change
|
|
// to the selection, the user will drag the first caret.
|
|
mActiveCaret = mFirstCaret.get();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
AccessibleCaretManager::CompareTreePosition(nsIFrame* aStartFrame,
|
|
nsIFrame* aEndFrame) const
|
|
{
|
|
return (aStartFrame && aEndFrame &&
|
|
nsLayoutUtils::CompareTreePosition(aStartFrame, aEndFrame) <= 0);
|
|
}
|
|
|
|
nsresult
|
|
AccessibleCaretManager::DragCaretInternal(const nsPoint& aPoint)
|
|
{
|
|
if (!mPresShell) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
nsIFrame* rootFrame = mPresShell->GetRootFrame();
|
|
if (!rootFrame) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
nsPoint point = AdjustDragBoundary(aPoint);
|
|
|
|
// Find out which content we point to
|
|
nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(
|
|
rootFrame, point,
|
|
nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
|
|
if (!ptFrame) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
RefPtr<nsFrameSelection> fs = GetFrameSelection();
|
|
if (!fs) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
nsresult result;
|
|
nsIFrame* newFrame = nullptr;
|
|
nsPoint newPoint;
|
|
nsPoint ptInFrame = point;
|
|
nsLayoutUtils::TransformPoint(rootFrame, ptFrame, ptInFrame);
|
|
result = fs->ConstrainFrameAndPointToAnchorSubtree(ptFrame, ptInFrame,
|
|
&newFrame, newPoint);
|
|
if (NS_FAILED(result) || !newFrame) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
bool selectable;
|
|
newFrame->IsSelectable(&selectable, nullptr);
|
|
if (!selectable) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
nsIFrame::ContentOffsets offsets =
|
|
newFrame->GetContentOffsetsFromPoint(newPoint);
|
|
if (offsets.IsNull()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
Selection* selection = GetSelection();
|
|
if (!selection) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
if (GetCaretMode() == CaretMode::Selection &&
|
|
!RestrictCaretDraggingOffsets(offsets)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
ClearMaintainedSelection();
|
|
|
|
nsIFrame* anchorFrame = nullptr;
|
|
selection->GetPrimaryFrameForAnchorNode(&anchorFrame);
|
|
|
|
nsIFrame* scrollable =
|
|
nsLayoutUtils::GetClosestFrameOfType(anchorFrame, nsGkAtoms::scrollFrame);
|
|
nsWeakFrame weakScrollable = scrollable;
|
|
fs->HandleClick(offsets.content, offsets.StartOffset(), offsets.EndOffset(),
|
|
GetCaretMode() == CaretMode::Selection, false,
|
|
offsets.associate);
|
|
if (!weakScrollable.IsAlive()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Scroll scrolled frame.
|
|
nsIScrollableFrame* saf = do_QueryFrame(scrollable);
|
|
nsIFrame* capturingFrame = saf->GetScrolledFrame();
|
|
nsPoint ptInScrolled = point;
|
|
nsLayoutUtils::TransformPoint(rootFrame, capturingFrame, ptInScrolled);
|
|
fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, kAutoScrollTimerDelay);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsRect
|
|
AccessibleCaretManager::GetAllChildFrameRectsUnion(nsIFrame* aFrame) const
|
|
{
|
|
nsRect unionRect;
|
|
|
|
// Drill through scroll frames, we don't want to include scrollbar child
|
|
// frames below.
|
|
for (nsIFrame* frame = aFrame->GetContentInsertionFrame();
|
|
frame;
|
|
frame = frame->GetNextContinuation()) {
|
|
nsRect frameRect;
|
|
|
|
for (nsIFrame::ChildListIterator lists(frame); !lists.IsDone(); lists.Next()) {
|
|
// Loop all children to union their scrollable overflow rect.
|
|
for (nsIFrame* child : lists.CurrentList()) {
|
|
nsRect childRect = child->GetScrollableOverflowRectRelativeToSelf();
|
|
nsLayoutUtils::TransformRect(child, frame, childRect);
|
|
|
|
// A TextFrame containing only '\n' has positive height and width 0, or
|
|
// positive width and height 0 if it's vertical. Need to use UnionEdges
|
|
// to add its rect. BRFrame rect should be non-empty.
|
|
if (childRect.IsEmpty()) {
|
|
frameRect = frameRect.UnionEdges(childRect);
|
|
} else {
|
|
frameRect = frameRect.Union(childRect);
|
|
}
|
|
}
|
|
}
|
|
|
|
MOZ_ASSERT(!frameRect.IsEmpty(),
|
|
"Editable frames should have at least one BRFrame child to make "
|
|
"frameRect non-empty!");
|
|
if (frame != aFrame) {
|
|
nsLayoutUtils::TransformRect(frame, aFrame, frameRect);
|
|
}
|
|
unionRect = unionRect.Union(frameRect);
|
|
}
|
|
|
|
return unionRect;
|
|
}
|
|
|
|
nsPoint
|
|
AccessibleCaretManager::AdjustDragBoundary(const nsPoint& aPoint) const
|
|
{
|
|
nsPoint adjustedPoint = aPoint;
|
|
|
|
int32_t focusOffset = 0;
|
|
nsIFrame* focusFrame =
|
|
nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &focusOffset);
|
|
Element* editingHost = GetEditingHostForFrame(focusFrame);
|
|
|
|
if (editingHost) {
|
|
nsIFrame* editingHostFrame = editingHost->GetPrimaryFrame();
|
|
if (editingHostFrame) {
|
|
nsRect boundary = GetAllChildFrameRectsUnion(editingHostFrame);
|
|
nsLayoutUtils::TransformRect(editingHostFrame, mPresShell->GetRootFrame(),
|
|
boundary);
|
|
|
|
// Shrink the rect to make sure we never hit the boundary.
|
|
boundary.Deflate(kBoundaryAppUnits);
|
|
|
|
adjustedPoint = boundary.ClampPoint(adjustedPoint);
|
|
}
|
|
}
|
|
|
|
if (GetCaretMode() == CaretMode::Selection &&
|
|
!sCaretsAllowDraggingAcrossOtherCaret) {
|
|
// Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt
|
|
// mode when a caret is being dragged surpass the other caret.
|
|
//
|
|
// For example, when dragging the second caret, the horizontal boundary (lower
|
|
// bound) of its Y-coordinate is the logical position of the first caret.
|
|
// Likewise, when dragging the first caret, the horizontal boundary (upper
|
|
// bound) of its Y-coordinate is the logical position of the second caret.
|
|
if (mActiveCaret == mFirstCaret.get()) {
|
|
nscoord dragDownBoundaryY = mSecondCaret->LogicalPosition().y;
|
|
if (dragDownBoundaryY > 0 && adjustedPoint.y > dragDownBoundaryY) {
|
|
adjustedPoint.y = dragDownBoundaryY;
|
|
}
|
|
} else {
|
|
nscoord dragUpBoundaryY = mFirstCaret->LogicalPosition().y;
|
|
if (adjustedPoint.y < dragUpBoundaryY) {
|
|
adjustedPoint.y = dragUpBoundaryY;
|
|
}
|
|
}
|
|
}
|
|
|
|
return adjustedPoint;
|
|
}
|
|
|
|
uint32_t
|
|
AccessibleCaretManager::CaretTimeoutMs() const
|
|
{
|
|
static bool added = false;
|
|
static uint32_t caretTimeoutMs = 0;
|
|
|
|
if (!added) {
|
|
Preferences::AddUintVarCache(&caretTimeoutMs,
|
|
"layout.accessiblecaret.timeout_ms");
|
|
added = true;
|
|
}
|
|
|
|
return caretTimeoutMs;
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::LaunchCaretTimeoutTimer()
|
|
{
|
|
if (!mPresShell || !mCaretTimeoutTimer || CaretTimeoutMs() == 0 ||
|
|
GetCaretMode() != CaretMode::Cursor || mActiveCaret) {
|
|
return;
|
|
}
|
|
|
|
nsTimerCallbackFunc callback = [](nsITimer* aTimer, void* aClosure) {
|
|
auto self = static_cast<AccessibleCaretManager*>(aClosure);
|
|
if (self->GetCaretMode() == CaretMode::Cursor) {
|
|
self->HideCarets();
|
|
}
|
|
};
|
|
|
|
mCaretTimeoutTimer->InitWithFuncCallback(callback, this, CaretTimeoutMs(),
|
|
nsITimer::TYPE_ONE_SHOT);
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::CancelCaretTimeoutTimer()
|
|
{
|
|
if (mCaretTimeoutTimer) {
|
|
mCaretTimeoutTimer->Cancel();
|
|
}
|
|
}
|
|
|
|
void
|
|
AccessibleCaretManager::DispatchCaretStateChangedEvent(CaretChangedReason aReason) const
|
|
{
|
|
if (!mPresShell) {
|
|
return;
|
|
}
|
|
|
|
FlushLayout();
|
|
if (IsTerminated()) {
|
|
return;
|
|
}
|
|
|
|
Selection* sel = GetSelection();
|
|
if (!sel) {
|
|
return;
|
|
}
|
|
|
|
nsIDocument* doc = mPresShell->GetDocument();
|
|
MOZ_ASSERT(doc);
|
|
|
|
CaretStateChangedEventInit init;
|
|
init.mBubbles = true;
|
|
|
|
const nsRange* range = sel->GetAnchorFocusRange();
|
|
nsINode* commonAncestorNode = nullptr;
|
|
if (range) {
|
|
commonAncestorNode = range->GetCommonAncestor();
|
|
}
|
|
|
|
if (!commonAncestorNode) {
|
|
commonAncestorNode = sel->GetFrameSelection()->GetAncestorLimiter();
|
|
}
|
|
|
|
RefPtr<DOMRect> domRect = new DOMRect(ToSupports(doc));
|
|
nsRect rect = nsLayoutUtils::GetSelectionBoundingRect(sel);
|
|
|
|
nsIFrame* commonAncestorFrame = nullptr;
|
|
nsIFrame* rootFrame = mPresShell->GetRootFrame();
|
|
|
|
if (commonAncestorNode && commonAncestorNode->IsContent()) {
|
|
commonAncestorFrame = commonAncestorNode->AsContent()->GetPrimaryFrame();
|
|
}
|
|
|
|
if (commonAncestorFrame && rootFrame) {
|
|
nsLayoutUtils::TransformRect(rootFrame, commonAncestorFrame, rect);
|
|
nsRect clampedRect = nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame,
|
|
rect);
|
|
nsLayoutUtils::TransformRect(commonAncestorFrame, rootFrame, clampedRect);
|
|
domRect->SetLayoutRect(clampedRect);
|
|
init.mSelectionVisible = !clampedRect.IsEmpty();
|
|
} else {
|
|
domRect->SetLayoutRect(rect);
|
|
init.mSelectionVisible = true;
|
|
}
|
|
|
|
// Send isEditable info w/ event detail. This info can help determine
|
|
// whether to show cut command on selection dialog or not.
|
|
init.mSelectionEditable = commonAncestorFrame &&
|
|
GetEditingHostForFrame(commonAncestorFrame);
|
|
|
|
init.mBoundingClientRect = domRect;
|
|
init.mReason = aReason;
|
|
init.mCollapsed = sel->IsCollapsed();
|
|
init.mCaretVisible = mFirstCaret->IsLogicallyVisible() ||
|
|
mSecondCaret->IsLogicallyVisible();
|
|
init.mCaretVisuallyVisible = mFirstCaret->IsVisuallyVisible() ||
|
|
mSecondCaret->IsVisuallyVisible();
|
|
sel->Stringify(init.mSelectedTextContent);
|
|
|
|
RefPtr<CaretStateChangedEvent> event =
|
|
CaretStateChangedEvent::Constructor(doc, NS_LITERAL_STRING("mozcaretstatechanged"), init);
|
|
|
|
event->SetTrusted(true);
|
|
event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
|
|
|
|
AC_LOG("%s: reason %d, collapsed %d, caretVisible %d", __FUNCTION__,
|
|
init.mReason, init.mCollapsed, init.mCaretVisible);
|
|
|
|
(new AsyncEventDispatcher(doc, event))->RunDOMEventWhenSafe();
|
|
}
|
|
|
|
} // namespace mozilla
|