AccessibleCaretManager.cpp (56666B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "AccessibleCaretManager.h" 8 9 #include <utility> 10 11 #include "AccessibleCaret.h" 12 #include "AccessibleCaretEventHub.h" 13 #include "AccessibleCaretLogger.h" 14 #include "mozilla/AsyncEventDispatcher.h" 15 #include "mozilla/AutoRestore.h" 16 #include "mozilla/CaretAssociationHint.h" 17 #include "mozilla/ContentIterator.h" 18 #include "mozilla/FocusModel.h" 19 #include "mozilla/IMEStateManager.h" 20 #include "mozilla/IntegerPrintfMacros.h" 21 #include "mozilla/PresShell.h" 22 #include "mozilla/ScrollContainerFrame.h" 23 #include "mozilla/SelectionMovementUtils.h" 24 #include "mozilla/StaticAnalysisFunctions.h" 25 #include "mozilla/StaticPrefs_layout.h" 26 #include "mozilla/dom/Element.h" 27 #include "mozilla/dom/MouseEventBinding.h" 28 #include "mozilla/dom/NodeFilterBinding.h" 29 #include "mozilla/dom/Selection.h" 30 #include "mozilla/dom/TreeWalker.h" 31 #include "nsCaret.h" 32 #include "nsContainerFrame.h" 33 #include "nsContentUtils.h" 34 #include "nsDebug.h" 35 #include "nsFocusManager.h" 36 #include "nsFrameSelection.h" 37 #include "nsGenericHTMLElement.h" 38 #include "nsIFrame.h" 39 #include "nsIHapticFeedback.h" 40 #include "nsLayoutUtils.h" 41 #include "nsServiceManagerUtils.h" 42 43 namespace mozilla { 44 45 #undef AC_LOG 46 #define AC_LOG(message, ...) \ 47 AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__); 48 49 #undef AC_LOGV 50 #define AC_LOGV(message, ...) \ 51 AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__); 52 53 using namespace dom; 54 using Appearance = AccessibleCaret::Appearance; 55 using PositionChangedResult = AccessibleCaret::PositionChangedResult; 56 57 #define AC_PROCESS_ENUM_TO_STREAM(e) \ 58 case (e): \ 59 aStream << #e; \ 60 break; 61 std::ostream& operator<<(std::ostream& aStream, 62 const AccessibleCaretManager::CaretMode& aCaretMode) { 63 using CaretMode = AccessibleCaretManager::CaretMode; 64 switch (aCaretMode) { 65 AC_PROCESS_ENUM_TO_STREAM(CaretMode::None); 66 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor); 67 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection); 68 } 69 return aStream; 70 } 71 72 std::ostream& operator<<( 73 std::ostream& aStream, 74 const AccessibleCaretManager::UpdateCaretsHint& aHint) { 75 using UpdateCaretsHint = AccessibleCaretManager::UpdateCaretsHint; 76 switch (aHint) { 77 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default); 78 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance); 79 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent); 80 } 81 return aStream; 82 } 83 #undef AC_PROCESS_ENUM_TO_STREAM 84 85 AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell) 86 : AccessibleCaretManager{ 87 aPresShell, 88 Carets{aPresShell ? MakeUnique<AccessibleCaret>(aPresShell) : nullptr, 89 aPresShell ? MakeUnique<AccessibleCaret>(aPresShell) 90 : nullptr}} {} 91 92 AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell, 93 Carets aCarets) 94 : mPresShell{aPresShell}, mCarets{std::move(aCarets)} {} 95 96 AccessibleCaretManager::LayoutFlusher::~LayoutFlusher() { 97 MOZ_RELEASE_ASSERT(!mFlushing, "Going away in MaybeFlush? Bad!"); 98 } 99 100 void AccessibleCaretManager::Terminate() { 101 mCarets.Terminate(); 102 mActiveCaret = nullptr; 103 mPresShell = nullptr; 104 } 105 106 nsresult AccessibleCaretManager::OnSelectionChanged(Document* aDoc, 107 Selection* aSel, 108 int16_t aReason) { 109 Selection* selection = GetSelection(); 110 AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__, aSel, 111 selection, aReason); 112 if (aSel != selection) { 113 return NS_OK; 114 } 115 116 // eSetSelection events from the Fennec widget IME can be generated 117 // by autoSuggest / autoCorrect composition changes, or by TYPE_REPLACE_TEXT 118 // actions, either positioning cursor for text insert, or selecting 119 // text-to-be-replaced. None should affect AccessibleCaret visibility. 120 if (aReason & nsISelectionListener::IME_REASON) { 121 return NS_OK; 122 } 123 124 // Move the cursor by JavaScript or unknown internal call. 125 if (aReason == nsISelectionListener::NO_REASON || 126 aReason == nsISelectionListener::JS_REASON) { 127 auto mode = static_cast<ScriptUpdateMode>( 128 StaticPrefs::layout_accessiblecaret_script_change_update_mode()); 129 if (mode == kScriptAlwaysShow || 130 (mode == kScriptUpdateVisible && mCarets.HasLogicallyVisibleCaret())) { 131 UpdateCarets(); 132 return NS_OK; 133 } 134 // Default for NO_REASON is to make hidden. 135 HideCaretsAndDispatchCaretStateChangedEvent(); 136 return NS_OK; 137 } 138 139 // Move cursor by keyboard. 140 if (aReason & nsISelectionListener::KEYPRESS_REASON) { 141 HideCaretsAndDispatchCaretStateChangedEvent(); 142 return NS_OK; 143 } 144 145 // OnBlur() might be called between mouse down and mouse up, so we hide carets 146 // upon mouse down anyway, and update carets upon mouse up. 147 if (aReason & nsISelectionListener::MOUSEDOWN_REASON) { 148 HideCaretsAndDispatchCaretStateChangedEvent(); 149 return NS_OK; 150 } 151 152 // Range will collapse after cutting or copying text. 153 if (aReason & (nsISelectionListener::COLLAPSETOSTART_REASON | 154 nsISelectionListener::COLLAPSETOEND_REASON)) { 155 HideCaretsAndDispatchCaretStateChangedEvent(); 156 return NS_OK; 157 } 158 159 // For mouse input we don't want to show the carets. 160 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() && 161 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) { 162 HideCaretsAndDispatchCaretStateChangedEvent(); 163 return NS_OK; 164 } 165 166 // When we want to hide the carets for mouse input, hide them for select 167 // all action fired by keyboard as well. 168 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() && 169 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_KEYBOARD && 170 (aReason & nsISelectionListener::SELECTALL_REASON)) { 171 HideCaretsAndDispatchCaretStateChangedEvent(); 172 return NS_OK; 173 } 174 175 UpdateCarets(); 176 return NS_OK; 177 } 178 179 void AccessibleCaretManager::HideCaretsAndDispatchCaretStateChangedEvent() { 180 if (mCarets.HasLogicallyVisibleCaret()) { 181 AC_LOG("%s", __FUNCTION__); 182 mCarets.GetFirst()->SetAppearance(Appearance::None); 183 mCarets.GetSecond()->SetAppearance(Appearance::None); 184 mIsCaretPositionChanged = false; 185 mDesiredAsyncPanZoomState.Update(*this); 186 DispatchCaretStateChangedEvent(CaretChangedReason::Visibilitychange); 187 } 188 } 189 190 auto AccessibleCaretManager::MaybeFlushLayout() -> Terminated { 191 if (mPresShell) { 192 // `MaybeFlush` doesn't access the PresShell after flushing, so it's OK to 193 // mark it as live. 194 mLayoutFlusher.MaybeFlush(MOZ_KnownLive(*mPresShell)); 195 } 196 197 return IsTerminated(); 198 } 199 200 void AccessibleCaretManager::UpdateCarets(const UpdateCaretsHintSet& aHint) { 201 if (MaybeFlushLayout() == Terminated::Yes) { 202 return; 203 } 204 205 mLastUpdateCaretMode = GetCaretMode(); 206 207 switch (mLastUpdateCaretMode) { 208 case CaretMode::None: 209 HideCaretsAndDispatchCaretStateChangedEvent(); 210 break; 211 case CaretMode::Cursor: 212 UpdateCaretsForCursorMode(aHint); 213 break; 214 case CaretMode::Selection: 215 UpdateCaretsForSelectionMode(aHint); 216 break; 217 } 218 219 mDesiredAsyncPanZoomState.Update(*this); 220 } 221 222 bool AccessibleCaretManager::IsCaretDisplayableInCursorMode( 223 nsIFrame** aOutFrame, int32_t* aOutOffset) const { 224 RefPtr<nsCaret> caret = mPresShell->GetCaret(); 225 if (!caret || !caret->IsVisible()) { 226 return false; 227 } 228 auto frameData = 229 nsCaret::GetFrameAndOffset(nsCaret::CaretPositionFor(GetSelection())); 230 if (!GetEditingHostForFrame(frameData.mFrame)) { 231 return false; 232 } 233 if (aOutFrame) { 234 *aOutFrame = frameData.mFrame; 235 } 236 if (aOutOffset) { 237 *aOutOffset = frameData.mOffsetInFrameContent; 238 } 239 return true; 240 } 241 242 bool AccessibleCaretManager::HasNonEmptyTextContent(nsINode* aNode) const { 243 return nsContentUtils::HasNonEmptyTextContent( 244 aNode, nsContentUtils::eRecurseIntoChildren); 245 } 246 247 void AccessibleCaretManager::UpdateCaretsForCursorMode( 248 const UpdateCaretsHintSet& aHints) { 249 AC_LOG("%s, selection: %p", __FUNCTION__, GetSelection()); 250 251 int32_t offset = 0; 252 nsIFrame* frame = nullptr; 253 if (!IsCaretDisplayableInCursorMode(&frame, &offset)) { 254 HideCaretsAndDispatchCaretStateChangedEvent(); 255 return; 256 } 257 258 PositionChangedResult result = mCarets.GetFirst()->SetPosition(frame, offset); 259 260 switch (result) { 261 case PositionChangedResult::NotChanged: 262 case PositionChangedResult::Position: 263 case PositionChangedResult::Zoom: 264 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) { 265 if (HasNonEmptyTextContent(GetEditingHostForFrame(frame))) { 266 mCarets.GetFirst()->SetAppearance(Appearance::Normal); 267 } else if ( 268 StaticPrefs:: 269 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) { 270 if (mCarets.GetFirst()->IsLogicallyVisible()) { 271 // Possible cases are: 1) SelectWordOrShortcut() sets the 272 // appearance to Normal. 2) When the caret is out of viewport and 273 // now scrolling into viewport, it has appearance NormalNotShown. 274 mCarets.GetFirst()->SetAppearance(Appearance::Normal); 275 } else { 276 // Possible cases are: a) Single tap on current empty content; 277 // OnSelectionChanged() sets the appearance to None due to 278 // MOUSEDOWN_REASON. b) Single tap on other empty content; 279 // OnBlur() sets the appearance to None. 280 // 281 // Do nothing to make the appearance remains None so that it can 282 // be distinguished from case 2). Also do not set the appearance 283 // to NormalNotShown here like the default update behavior. 284 } 285 } else { 286 mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown); 287 } 288 } 289 break; 290 291 case PositionChangedResult::Invisible: 292 mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown); 293 break; 294 } 295 296 mCarets.GetSecond()->SetAppearance(Appearance::None); 297 298 mIsCaretPositionChanged = (result == PositionChangedResult::Position); 299 300 if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) { 301 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition); 302 } 303 } 304 305 void AccessibleCaretManager::UpdateCaretsForSelectionMode( 306 const UpdateCaretsHintSet& aHints) { 307 AC_LOG("%s: selection: %p", __FUNCTION__, GetSelection()); 308 309 // NOTE: Here needs to call CompareTreePosition() which is overridden by 310 // MockAccessibleCaretManager() to make it always return true. Therefore, 311 // we cannot return earlier when mPresShell is nullptr or the result of 312 // GetFrameForRangeStart() is nullptr. 313 const FrameAndOffset startFrameAndOffset = 314 mPresShell ? GetFirstVisibleLeafFrameOrUnselectableChildFrame( 315 *GetSelection()->GetFirstRange()) 316 : FrameAndOffset{}; 317 nsCOMPtr<nsIContent> endContent; 318 const FrameAndOffset endFrameAndOffset = 319 mPresShell 320 ? GetLastVisibleLeafFrameOrUnselectableChildFrame( 321 *GetSelection()->GetLastRange(), getter_AddRefs(endContent)) 322 : FrameAndOffset{}; 323 324 if (!CompareTreePosition( 325 startFrameAndOffset.mFrame, 326 static_cast<int32_t>(startFrameAndOffset.mOffsetInFrameContent), 327 endFrameAndOffset.mFrame, 328 static_cast<int32_t>(endFrameAndOffset.mOffsetInFrameContent))) { 329 // XXX: Do we really have to hide carets if this condition isn't satisfied? 330 HideCaretsAndDispatchCaretStateChangedEvent(); 331 return; 332 } 333 334 auto updateSingleCaret = [aHints](AccessibleCaret* aCaret, nsIFrame* aFrame, 335 int32_t aOffset) -> PositionChangedResult { 336 PositionChangedResult result = aCaret->SetPosition(aFrame, aOffset); 337 338 switch (result) { 339 case PositionChangedResult::NotChanged: 340 case PositionChangedResult::Position: 341 case PositionChangedResult::Zoom: 342 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) { 343 aCaret->SetAppearance(Appearance::Normal); 344 } 345 break; 346 347 case PositionChangedResult::Invisible: 348 aCaret->SetAppearance(Appearance::NormalNotShown); 349 break; 350 } 351 return result; 352 }; 353 354 PositionChangedResult firstCaretResult = updateSingleCaret( 355 mCarets.GetFirst(), startFrameAndOffset.mFrame, 356 static_cast<int32_t>(startFrameAndOffset.mOffsetInFrameContent)); 357 // If we get a frame for a child node for the end boundary, e.g., when the 358 // last visible content is <img> or something or unselectable container, 359 // we want to put the second caret to next to its end edge. Then, we use 360 // the specific behavior of nsIFrame::GetPointFromOffset() (called by 361 // nsCaret::GetGeometryForFrame() in AccessibleCaret::SetPosition()) which 362 // returns the end edge if we set the length of frame content + 1. 363 const uint32_t offsetInEndFrameContent = 364 endFrameAndOffset.GetFrameContent() == endContent 365 ? endFrameAndOffset.mOffsetInFrameContent 366 : endFrameAndOffset.GetFrameContent()->Length() + 1; 367 PositionChangedResult secondCaretResult = 368 updateSingleCaret(mCarets.GetSecond(), endFrameAndOffset.mFrame, 369 static_cast<int32_t>(offsetInEndFrameContent)); 370 371 mIsCaretPositionChanged = 372 firstCaretResult == PositionChangedResult::Position || 373 secondCaretResult == PositionChangedResult::Position; 374 375 if (mIsCaretPositionChanged) { 376 // Flush layout to make the carets intersection correct. 377 if (MaybeFlushLayout() == Terminated::Yes) { 378 return; 379 } 380 } 381 382 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) { 383 // Only check for tilt carets when the caller doesn't ask us to preserve 384 // old appearance. Otherwise we might override the appearance set by the 385 // caller. 386 if (StaticPrefs::layout_accessiblecaret_always_tilt()) { 387 UpdateCaretsForAlwaysTilt(startFrameAndOffset.mFrame, 388 endFrameAndOffset.mFrame); 389 } else { 390 UpdateCaretsForOverlappingTilt(); 391 } 392 } 393 394 if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) { 395 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition); 396 } 397 } 398 399 void AccessibleCaretManager::DesiredAsyncPanZoomState::Update( 400 const AccessibleCaretManager& aAccessibleCaretManager) { 401 if (aAccessibleCaretManager.mActiveCaret) { 402 // No need to disable APZ when dragging the caret. 403 mValue = Value::Enabled; 404 return; 405 } 406 407 if (aAccessibleCaretManager.mIsScrollStarted) { 408 // During scrolling, the caret's position is changed only if it is in a 409 // position:fixed or a "stuck" position:sticky frame subtree. 410 mValue = aAccessibleCaretManager.mIsCaretPositionChanged ? Value::Disabled 411 : Value::Enabled; 412 return; 413 } 414 415 // For other cases, we can only reliably detect whether the caret is in a 416 // position:fixed frame subtree. 417 switch (aAccessibleCaretManager.mLastUpdateCaretMode) { 418 case CaretMode::None: 419 mValue = Value::Enabled; 420 break; 421 case CaretMode::Cursor: 422 mValue = 423 (aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() && 424 aAccessibleCaretManager.mCarets.GetFirst() 425 ->IsInPositionFixedSubtree()) 426 ? Value::Disabled 427 : Value::Enabled; 428 break; 429 case CaretMode::Selection: 430 mValue = 431 ((aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() && 432 aAccessibleCaretManager.mCarets.GetFirst() 433 ->IsInPositionFixedSubtree()) || 434 (aAccessibleCaretManager.mCarets.GetSecond()->IsVisuallyVisible() && 435 aAccessibleCaretManager.mCarets.GetSecond() 436 ->IsInPositionFixedSubtree())) 437 ? Value::Disabled 438 : Value::Enabled; 439 break; 440 } 441 } 442 443 bool AccessibleCaretManager::UpdateCaretsForOverlappingTilt() { 444 if (!mCarets.GetFirst()->IsVisuallyVisible() || 445 !mCarets.GetSecond()->IsVisuallyVisible()) { 446 return false; 447 } 448 449 if (!mCarets.GetFirst()->Intersects(*mCarets.GetSecond())) { 450 mCarets.GetFirst()->SetAppearance(Appearance::Normal); 451 mCarets.GetSecond()->SetAppearance(Appearance::Normal); 452 return false; 453 } 454 455 if (mCarets.GetFirst()->LogicalPosition().x <= 456 mCarets.GetSecond()->LogicalPosition().x) { 457 mCarets.GetFirst()->SetAppearance(Appearance::Left); 458 mCarets.GetSecond()->SetAppearance(Appearance::Right); 459 } else { 460 mCarets.GetFirst()->SetAppearance(Appearance::Right); 461 mCarets.GetSecond()->SetAppearance(Appearance::Left); 462 } 463 464 return true; 465 } 466 467 void AccessibleCaretManager::UpdateCaretsForAlwaysTilt( 468 const nsIFrame* aStartFrame, const nsIFrame* aEndFrame) { 469 // When a short LTR word in RTL environment is selected, the two carets 470 // tilted inward might be overlapped. Make them tilt outward. 471 if (UpdateCaretsForOverlappingTilt()) { 472 return; 473 } 474 475 if (mCarets.GetFirst()->IsVisuallyVisible()) { 476 auto startFrameWritingMode = aStartFrame->GetWritingMode(); 477 mCarets.GetFirst()->SetAppearance(startFrameWritingMode.IsBidiLTR() 478 ? Appearance::Left 479 : Appearance::Right); 480 } 481 if (mCarets.GetSecond()->IsVisuallyVisible()) { 482 auto endFrameWritingMode = aEndFrame->GetWritingMode(); 483 mCarets.GetSecond()->SetAppearance( 484 endFrameWritingMode.IsBidiLTR() ? Appearance::Right : Appearance::Left); 485 } 486 } 487 488 void AccessibleCaretManager::ProvideHapticFeedback() { 489 if (StaticPrefs::layout_accessiblecaret_hapticfeedback()) { 490 if (nsCOMPtr<nsIHapticFeedback> haptic = 491 do_GetService("@mozilla.org/widget/hapticfeedback;1")) { 492 haptic->PerformSimpleAction(haptic->LongPress); 493 } 494 } 495 } 496 497 nsresult AccessibleCaretManager::PressCaret(const nsPoint& aPoint, 498 EventClassID aEventClass) { 499 nsresult rv = NS_ERROR_FAILURE; 500 501 MOZ_ASSERT(aEventClass == eMouseEventClass || aEventClass == eTouchEventClass, 502 "Unexpected event class!"); 503 504 using TouchArea = AccessibleCaret::TouchArea; 505 TouchArea touchArea = 506 aEventClass == eMouseEventClass ? TouchArea::CaretImage : TouchArea::Full; 507 508 if (mCarets.GetFirst()->Contains(aPoint, touchArea)) { 509 mActiveCaret = mCarets.GetFirst(); 510 SetSelectionDirection(eDirPrevious); 511 } else if (mCarets.GetSecond()->Contains(aPoint, touchArea)) { 512 mActiveCaret = mCarets.GetSecond(); 513 SetSelectionDirection(eDirNext); 514 } 515 516 if (mActiveCaret) { 517 mOffsetYToCaretLogicalPosition = 518 mActiveCaret->LogicalPosition().y - aPoint.y; 519 SetSelectionDragState(true); 520 DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret, &aPoint); 521 rv = NS_OK; 522 } 523 524 return rv; 525 } 526 527 nsresult AccessibleCaretManager::DragCaret(const nsPoint& aPoint) { 528 MOZ_ASSERT(mActiveCaret); 529 MOZ_ASSERT(GetCaretMode() != CaretMode::None); 530 531 if (!mPresShell || !mPresShell->GetRootFrame() || !GetSelection()) { 532 return NS_ERROR_NULL_POINTER; 533 } 534 535 StopSelectionAutoScrollTimer(); 536 DragCaretInternal(aPoint); 537 538 // We want to scroll the page even if we failed to drag the caret. 539 StartSelectionAutoScrollTimer(aPoint); 540 UpdateCarets(); 541 542 if (StaticPrefs::layout_accessiblecaret_magnifier_enabled()) { 543 DispatchCaretStateChangedEvent(CaretChangedReason::Dragcaret, &aPoint); 544 } 545 return NS_OK; 546 } 547 548 nsresult AccessibleCaretManager::ReleaseCaret() { 549 MOZ_ASSERT(mActiveCaret); 550 551 mActiveCaret = nullptr; 552 SetSelectionDragState(false); 553 mDesiredAsyncPanZoomState.Update(*this); 554 DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret); 555 return NS_OK; 556 } 557 558 nsresult AccessibleCaretManager::TapCaret(const nsPoint& aPoint) { 559 MOZ_ASSERT(GetCaretMode() != CaretMode::None); 560 561 nsresult rv = NS_ERROR_FAILURE; 562 563 if (GetCaretMode() == CaretMode::Cursor) { 564 DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret, &aPoint); 565 rv = NS_OK; 566 } 567 568 return rv; 569 } 570 571 static EnumSet<nsLayoutUtils::FrameForPointOption> GetHitTestOptions() { 572 EnumSet<nsLayoutUtils::FrameForPointOption> options = { 573 nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression, 574 nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc}; 575 return options; 576 } 577 578 nsresult AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint) { 579 // If the long-tap is landing on a pre-existing selection, don't replace 580 // it with a new one. Instead just return and let the context menu pop up 581 // on the pre-existing selection. 582 if (GetCaretMode() == CaretMode::Selection && 583 GetSelection()->ContainsPoint(aPoint)) { 584 AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__); 585 UpdateCarets(); 586 ProvideHapticFeedback(); 587 return NS_OK; 588 } 589 590 if (!mPresShell) { 591 return NS_ERROR_UNEXPECTED; 592 } 593 594 nsIFrame* rootFrame = mPresShell->GetRootFrame(); 595 if (!rootFrame) { 596 return NS_ERROR_NOT_AVAILABLE; 597 } 598 599 // Find the frame under point. 600 AutoWeakFrame ptFrame = nsLayoutUtils::GetFrameForPoint( 601 RelativeTo{rootFrame}, aPoint, GetHitTestOptions()); 602 if (!ptFrame.GetFrame()) { 603 return NS_ERROR_FAILURE; 604 } 605 606 nsIFrame* focusableFrame = GetFocusableFrame(ptFrame); 607 608 #ifdef DEBUG_FRAME_DUMP 609 AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__, ptFrame->ListTag().get(), 610 aPoint.x, aPoint.y); 611 AC_LOG("%s: Found %s focusable", __FUNCTION__, 612 focusableFrame ? focusableFrame->ListTag().get() : "no frame"); 613 #endif 614 615 // Get ptInFrame here so that we don't need to check whether rootFrame is 616 // alive later. Note that if ptFrame is being moved by 617 // IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below, 618 // something under the original point will be selected, which may not be the 619 // original text the user wants to select. 620 nsPoint ptInFrame = aPoint; 621 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame}, 622 ptInFrame); 623 624 // Firstly check long press on an empty editable content. 625 Element* newFocusEditingHost = GetEditingHostForFrame(ptFrame); 626 if (focusableFrame && newFocusEditingHost && 627 !HasNonEmptyTextContent(newFocusEditingHost)) { 628 ChangeFocusToOrClearOldFocus(focusableFrame); 629 630 if (StaticPrefs:: 631 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) { 632 mCarets.GetFirst()->SetAppearance(Appearance::Normal); 633 } 634 // We need to update carets to get correct information before dispatching 635 // CaretStateChangedEvent. 636 UpdateCarets(); 637 ProvideHapticFeedback(); 638 DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent); 639 return NS_OK; 640 } 641 642 bool selectable = ptFrame->IsSelectable(); 643 644 #ifdef DEBUG_FRAME_DUMP 645 AC_LOG("%s: %s %s selectable.", __FUNCTION__, ptFrame->ListTag().get(), 646 selectable ? "is" : "is NOT"); 647 #endif 648 649 if (!selectable) { 650 return NS_ERROR_FAILURE; 651 } 652 653 // Commit the composition string of the old editable focus element (if there 654 // is any) before changing the focus. 655 IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION, 656 mPresShell->GetPresContext()); 657 if (!ptFrame.IsAlive()) { 658 // Cannot continue because ptFrame died. 659 return NS_ERROR_FAILURE; 660 } 661 662 // ptFrame is selectable. Now change the focus. 663 ChangeFocusToOrClearOldFocus(focusableFrame); 664 if (!ptFrame.IsAlive()) { 665 // Cannot continue because ptFrame died. 666 return NS_ERROR_FAILURE; 667 } 668 669 // If long tap point isn't selectable frame for caret and frame selection 670 // can find a better frame for caret, we don't select a word. 671 // See https://webcompat.com/issues/15953 672 nsIFrame::ContentOffsets offsets = ptFrame->GetContentOffsetsFromPoint( 673 ptInFrame, 674 nsIFrame::SKIP_HIDDEN | nsIFrame::IGNORE_NATIVE_ANONYMOUS_SUBTREE); 675 if (offsets.content) { 676 RefPtr<nsFrameSelection> frameSelection = GetFrameSelection(); 677 if (frameSelection) { 678 const FrameAndOffset textFrameAndOffsetContainingWordBoundary = 679 SelectionMovementUtils::GetFrameForNodeOffset( 680 offsets.content, offsets.offset, offsets.associate); 681 if (textFrameAndOffsetContainingWordBoundary && 682 textFrameAndOffsetContainingWordBoundary != ptFrame) { 683 SetSelectionDragState(true); 684 frameSelection->HandleClick( 685 MOZ_KnownLive(offsets.content) /* bug 1636889 */, 686 offsets.StartOffset(), offsets.EndOffset(), 687 nsFrameSelection::FocusMode::kCollapseToNewPoint, 688 offsets.associate); 689 SetSelectionDragState(false); 690 ClearMaintainedSelection(); 691 692 if (StaticPrefs:: 693 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) { 694 mCarets.GetFirst()->SetAppearance(Appearance::Normal); 695 } 696 697 UpdateCarets(); 698 ProvideHapticFeedback(); 699 DispatchCaretStateChangedEvent( 700 CaretChangedReason::Longpressonemptycontent); 701 702 return NS_OK; 703 } 704 } 705 } 706 707 // Then try select a word under point. 708 nsresult rv = SelectWord(ptFrame, ptInFrame); 709 UpdateCarets(); 710 ProvideHapticFeedback(); 711 712 return rv; 713 } 714 715 void AccessibleCaretManager::OnScrollStart() { 716 AC_LOG("%s", __FUNCTION__); 717 718 nsAutoScriptBlocker scriptBlocker; 719 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing); 720 mLayoutFlusher.mAllowFlushing = false; 721 722 Maybe<PresShell::AutoAssertNoFlush> assert; 723 if (mPresShell) { 724 assert.emplace(*mPresShell); 725 } 726 727 mIsScrollStarted = true; 728 729 if (mCarets.HasLogicallyVisibleCaret()) { 730 // Dispatch the event only if one of the carets is logically visible like in 731 // HideCaretsAndDispatchCaretStateChangedEvent(). 732 DispatchCaretStateChangedEvent(CaretChangedReason::Scroll); 733 } 734 } 735 736 void AccessibleCaretManager::OnScrollEnd() { 737 nsAutoScriptBlocker scriptBlocker; 738 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing); 739 mLayoutFlusher.mAllowFlushing = false; 740 741 Maybe<PresShell::AutoAssertNoFlush> assert; 742 if (mPresShell) { 743 assert.emplace(*mPresShell); 744 } 745 746 mIsScrollStarted = false; 747 748 if (GetCaretMode() == CaretMode::Cursor) { 749 if (!mCarets.GetFirst()->IsLogicallyVisible()) { 750 // If the caret is hidden (Appearance::None) due to blur, no 751 // need to update it. 752 return; 753 } 754 } 755 756 // For mouse and keyboard input, we don't want to show the carets. 757 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() && 758 (mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE || 759 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_KEYBOARD)) { 760 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__); 761 HideCaretsAndDispatchCaretStateChangedEvent(); 762 return; 763 } 764 765 AC_LOG("%s: UpdateCarets()", __FUNCTION__); 766 UpdateCarets(); 767 } 768 769 void AccessibleCaretManager::OnScrollPositionChanged() { 770 nsAutoScriptBlocker scriptBlocker; 771 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing); 772 mLayoutFlusher.mAllowFlushing = false; 773 774 Maybe<PresShell::AutoAssertNoFlush> assert; 775 if (mPresShell) { 776 assert.emplace(*mPresShell); 777 } 778 779 if (mCarets.HasLogicallyVisibleCaret()) { 780 if (mIsScrollStarted) { 781 // We don't want extra CaretStateChangedEvents dispatched when user is 782 // scrolling the page. 783 AC_LOG("%s: UpdateCarets(RespectOldAppearance | DispatchNoEvent)", 784 __FUNCTION__); 785 UpdateCarets({UpdateCaretsHint::RespectOldAppearance, 786 UpdateCaretsHint::DispatchNoEvent}); 787 } else { 788 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__); 789 UpdateCarets(UpdateCaretsHint::RespectOldAppearance); 790 } 791 } 792 } 793 794 void AccessibleCaretManager::OnReflow() { 795 nsAutoScriptBlocker scriptBlocker; 796 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing); 797 mLayoutFlusher.mAllowFlushing = false; 798 799 Maybe<PresShell::AutoAssertNoFlush> assert; 800 if (mPresShell) { 801 assert.emplace(*mPresShell); 802 } 803 804 if (mCarets.HasLogicallyVisibleCaret()) { 805 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__); 806 UpdateCarets(UpdateCaretsHint::RespectOldAppearance); 807 } 808 } 809 810 void AccessibleCaretManager::OnBlur() { 811 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__); 812 HideCaretsAndDispatchCaretStateChangedEvent(); 813 } 814 815 void AccessibleCaretManager::OnKeyboardEvent() { 816 if (GetCaretMode() == CaretMode::Cursor) { 817 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__); 818 HideCaretsAndDispatchCaretStateChangedEvent(); 819 } 820 } 821 822 void AccessibleCaretManager::SetLastInputSource(uint16_t aInputSource) { 823 mLastInputSource = aInputSource; 824 } 825 826 bool AccessibleCaretManager::ShouldDisableApz() const { 827 return mDesiredAsyncPanZoomState.ShouldDisable(); 828 } 829 830 Selection* AccessibleCaretManager::GetSelection() const { 831 RefPtr<nsFrameSelection> fs = GetFrameSelection(); 832 if (!fs) { 833 return nullptr; 834 } 835 return &fs->NormalSelection(); 836 } 837 838 already_AddRefed<nsFrameSelection> AccessibleCaretManager::GetFrameSelection() 839 const { 840 if (!mPresShell) { 841 return nullptr; 842 } 843 844 // Prevent us from touching the nsFrameSelection associated with other 845 // PresShell. 846 RefPtr<nsFrameSelection> fs = mPresShell->GetLastFocusedFrameSelection(); 847 if (!fs || fs->GetPresShell() != mPresShell) { 848 return nullptr; 849 } 850 851 return fs.forget(); 852 } 853 854 nsAutoString AccessibleCaretManager::StringifiedSelection() const { 855 nsAutoString str; 856 RefPtr<Selection> selection = GetSelection(); 857 if (selection) { 858 selection->Stringify(str, CallerType::System, 859 mLayoutFlusher.mAllowFlushing 860 ? Selection::FlushFrames::Yes 861 : Selection::FlushFrames::No); 862 } 863 return str; 864 } 865 866 // static 867 Element* AccessibleCaretManager::GetEditingHostForFrame( 868 const nsIFrame* aFrame) { 869 if (!aFrame) { 870 return nullptr; 871 } 872 873 auto content = aFrame->GetContent(); 874 if (!content) { 875 return nullptr; 876 } 877 878 return content->GetEditingHost(); 879 } 880 881 AccessibleCaretManager::CaretMode AccessibleCaretManager::GetCaretMode() const { 882 const Selection* selection = GetSelection(); 883 if (!selection) { 884 return CaretMode::None; 885 } 886 887 const uint32_t rangeCount = selection->RangeCount(); 888 if (rangeCount <= 0) { 889 return CaretMode::None; 890 } 891 892 const nsFocusManager* fm = nsFocusManager::GetFocusManager(); 893 MOZ_ASSERT(fm); 894 if (fm->GetFocusedWindow() != mPresShell->GetDocument()->GetWindow()) { 895 // Hide carets if the window is not focused. 896 return CaretMode::None; 897 } 898 899 if (selection->IsCollapsed()) { 900 return CaretMode::Cursor; 901 } 902 903 return CaretMode::Selection; 904 } 905 906 nsIFrame* AccessibleCaretManager::GetFocusableFrame(nsIFrame* aFrame) const { 907 // This implementation is similar to EventStateManager::PostHandleEvent(). 908 // Look for the nearest enclosing focusable frame. 909 nsIFrame* focusableFrame = aFrame; 910 while (focusableFrame) { 911 if (focusableFrame->IsFocusable(IsFocusableFlags::WithMouse)) { 912 break; 913 } 914 focusableFrame = focusableFrame->GetParent(); 915 } 916 return focusableFrame; 917 } 918 919 void AccessibleCaretManager::ChangeFocusToOrClearOldFocus( 920 nsIFrame* aFrame) const { 921 RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager(); 922 MOZ_ASSERT(fm); 923 924 if (aFrame) { 925 nsIContent* focusableContent = aFrame->GetContent(); 926 MOZ_ASSERT(focusableContent, "Focusable frame must have content!"); 927 RefPtr<Element> focusableElement = Element::FromNode(focusableContent); 928 fm->SetFocus(focusableElement, nsIFocusManager::FLAG_BYLONGPRESS); 929 } else if (nsCOMPtr<nsPIDOMWindowOuter> win = 930 mPresShell->GetDocument()->GetWindow()) { 931 fm->ClearFocus(win); 932 fm->SetFocusedWindow(win); 933 } 934 } 935 936 nsresult AccessibleCaretManager::SelectWord(nsIFrame* aFrame, 937 const nsPoint& aPoint) const { 938 AC_LOGV("%s", __FUNCTION__); 939 940 SetSelectionDragState(true); 941 nsresult rs = 942 aFrame->SelectByTypeAtPoint(aPoint, eSelectWord, eSelectWord, 0); 943 944 SetSelectionDragState(false); 945 ClearMaintainedSelection(); 946 947 // Smart-select phone numbers if possible. 948 if (StaticPrefs::layout_accessiblecaret_extend_selection_for_phone_number()) { 949 SelectMoreIfPhoneNumber(); 950 } 951 952 return rs; 953 } 954 955 void AccessibleCaretManager::SetSelectionDragState(bool aState) const { 956 RefPtr<nsFrameSelection> fs = GetFrameSelection(); 957 if (fs) { 958 fs->SetDragState(aState); 959 } 960 } 961 962 bool AccessibleCaretManager::IsPhoneNumber(const nsAString& aCandidate) const { 963 RefPtr<Document> doc = mPresShell->GetDocument(); 964 nsAutoString phoneNumberRegex(u"(^\\+)?[0-9 ,\\-.\\(\\)*#pw]{1,30}$"_ns); 965 return nsContentUtils::IsPatternMatching(aCandidate, 966 std::move(phoneNumberRegex), doc) 967 .valueOr(false); 968 } 969 970 void AccessibleCaretManager::SelectMoreIfPhoneNumber() const { 971 if (IsPhoneNumber(StringifiedSelection())) { 972 SetSelectionDirection(eDirNext); 973 ExtendPhoneNumberSelection(u"forward"_ns); 974 975 SetSelectionDirection(eDirPrevious); 976 ExtendPhoneNumberSelection(u"backward"_ns); 977 978 SetSelectionDirection(eDirNext); 979 } 980 } 981 982 void AccessibleCaretManager::ExtendPhoneNumberSelection( 983 const nsAString& aDirection) const { 984 if (!mPresShell) { 985 return; 986 } 987 988 // Extend the phone number selection until we find a boundary. 989 RefPtr<Selection> selection = GetSelection(); 990 991 while (selection) { 992 const nsRange* anchorFocusRange = selection->GetAnchorFocusRange(); 993 if (!anchorFocusRange) { 994 return; 995 } 996 997 // Backup the anchor focus range since both anchor node and focus node might 998 // be changed after calling Selection::Modify(). 999 RefPtr<nsRange> oldAnchorFocusRange = anchorFocusRange->CloneRange(); 1000 1001 // Save current focus node, focus offset and the selected text so that 1002 // we can compare them with the modified ones later. 1003 nsINode* oldFocusNode = selection->GetFocusNode(); 1004 uint32_t oldFocusOffset = selection->FocusOffset(); 1005 nsAutoString oldSelectedText = StringifiedSelection(); 1006 1007 // Extend the selection by one char. 1008 selection->Modify(u"extend"_ns, aDirection, u"character"_ns); 1009 if (IsTerminated() == Terminated::Yes) { 1010 return; 1011 } 1012 1013 // If the selection didn't change, (can't extend further), we're done. 1014 if (selection->GetFocusNode() == oldFocusNode && 1015 selection->FocusOffset() == oldFocusOffset) { 1016 return; 1017 } 1018 1019 // If the changed selection isn't a valid phone number, we're done. 1020 // Also, if the selection was extended to a new block node, the string 1021 // returned by stringify() won't have a new line at the beginning or the 1022 // end of the string. Therefore, if either focus node or offset is 1023 // changed, but selected text is not changed, we're done, too. 1024 nsAutoString selectedText = StringifiedSelection(); 1025 1026 if (!IsPhoneNumber(selectedText) || oldSelectedText == selectedText) { 1027 // Backout the undesired selection extend, restore the old anchor focus 1028 // range before exit. 1029 selection->SetAnchorFocusToRange(oldAnchorFocusRange); 1030 return; 1031 } 1032 } 1033 } 1034 1035 void AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const { 1036 Selection* selection = GetSelection(); 1037 if (selection) { 1038 selection->AdjustAnchorFocusForMultiRange(aDir); 1039 } 1040 } 1041 1042 void AccessibleCaretManager::ClearMaintainedSelection() const { 1043 // Selection made by double-clicking for example will maintain the original 1044 // word selection. We should clear it so that we can drag caret freely. 1045 RefPtr<nsFrameSelection> fs = GetFrameSelection(); 1046 if (fs) { 1047 fs->MaintainSelection(eSelectNoAmount); 1048 } 1049 } 1050 1051 void AccessibleCaretManager::LayoutFlusher::MaybeFlush( 1052 const PresShell& aPresShell) { 1053 if (mAllowFlushing) { 1054 AutoRestore<bool> flushing(mFlushing); 1055 mFlushing = true; 1056 1057 if (Document* doc = aPresShell.GetDocument()) { 1058 doc->FlushPendingNotifications(FlushType::Layout); 1059 // Don't access the PresShell after flushing, it could've become invalid. 1060 } 1061 } 1062 } 1063 1064 static nsIFrame* GetChildFrameContainingOffset( 1065 nsIFrame* aChildFrame, uint32_t aOffsetInChildFrameContent, 1066 CaretAssociationHint aHint) { 1067 nsIFrame* frameAtOffset = nullptr; 1068 int32_t unused = 0; 1069 if (NS_WARN_IF(NS_FAILED(aChildFrame->GetChildFrameContainingOffset( 1070 static_cast<int32_t>(aOffsetInChildFrameContent), 1071 aHint == CaretAssociationHint::After, &unused, &frameAtOffset)))) { 1072 frameAtOffset = aChildFrame; 1073 } 1074 return frameAtOffset; 1075 } 1076 1077 FrameAndOffset 1078 AccessibleCaretManager::GetFirstVisibleLeafFrameOrUnselectableChildFrame( 1079 nsRange& aRange, nsIContent** aOutContent /* = nullptr */, 1080 int32_t* aOutOffsetInContent /* = nullptr */) const { 1081 if (!mPresShell) { 1082 return {}; 1083 } 1084 1085 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection); 1086 1087 // FYI: aRange may be collapsed if `Selection` has multiple ranges. 1088 if (MOZ_UNLIKELY(aRange.Collapsed())) { 1089 return {}; 1090 } 1091 1092 const RawRangeBoundary& shrunkenStart = 1093 SelectionMovementUtils::GetFirstVisiblePointAtLeaf(aRange); 1094 if (MOZ_UNLIKELY(!shrunkenStart.IsSet())) { 1095 return {}; 1096 } 1097 if (aOutContent) { 1098 if (nsIContent* const outContent = 1099 nsIContent::FromNode(shrunkenStart.GetContainer())) { 1100 *aOutContent = do_AddRef(outContent).take(); 1101 } 1102 } 1103 if (aOutOffsetInContent) { 1104 *aOutOffsetInContent = static_cast<int32_t>( 1105 *shrunkenStart.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets)); 1106 } 1107 if (nsIContent* const child = shrunkenStart.GetChildAtOffset()) { 1108 if (nsIFrame* const childFrame = child->GetPrimaryFrame()) { 1109 const uint32_t offsetInFrameContent = 0u; 1110 nsIFrame* const childFrameAtOffset = GetChildFrameContainingOffset( 1111 childFrame, offsetInFrameContent, CaretAssociationHint::After); 1112 MOZ_ASSERT(childFrameAtOffset); 1113 // If the child is a non-selectable container which has padding or border, 1114 // we want to put the caret before the start edge, but returning the frame 1115 // makes the caller will get rect in its content. Therefore, do not 1116 // return the position in the unselectable container frame. 1117 if (!childFrameAtOffset->IsInlineFrame() || 1118 childFrameAtOffset->IsSelfEmpty()) { 1119 return {childFrameAtOffset, offsetInFrameContent}; 1120 } 1121 } 1122 } 1123 nsIContent* const container = 1124 nsIContent::FromNode(shrunkenStart.GetContainer()); 1125 if (MOZ_UNLIKELY(!container)) { 1126 return {}; 1127 } 1128 nsIFrame* const frame = container->GetPrimaryFrame(); 1129 if (MOZ_UNLIKELY(!frame)) { 1130 return {}; 1131 } 1132 MOZ_ASSERT(frame->IsSelectable()); 1133 const uint32_t offsetInFrameContent = 1134 *shrunkenStart.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets); 1135 nsIFrame* const frameAtOffset = GetChildFrameContainingOffset( 1136 frame, offsetInFrameContent, CaretAssociationHint::After); 1137 MOZ_ASSERT(frameAtOffset); 1138 return {frameAtOffset, offsetInFrameContent}; 1139 } 1140 1141 FrameAndOffset 1142 AccessibleCaretManager::GetLastVisibleLeafFrameOrUnselectableChildFrame( 1143 nsRange& aRange, nsIContent** aOutContent /* = nullptr */, 1144 int32_t* aOutOffsetInContent /* = nullptr */) const { 1145 if (!mPresShell) { 1146 return {}; 1147 } 1148 1149 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection); 1150 1151 // FYI: aRange may be collapsed if `Selection` has multiple ranges. 1152 if (MOZ_UNLIKELY(aRange.Collapsed())) { 1153 return {}; 1154 } 1155 1156 const RawRangeBoundary& shrunkenEnd = 1157 SelectionMovementUtils::GetLastVisiblePointAtLeaf(aRange); 1158 if (MOZ_UNLIKELY(!shrunkenEnd.IsSet())) { 1159 return {}; 1160 } 1161 if (aOutContent) { 1162 if (nsIContent* const outContent = 1163 nsIContent::FromNode(shrunkenEnd.GetContainer())) { 1164 *aOutContent = do_AddRef(outContent).take(); 1165 } 1166 } 1167 if (aOutOffsetInContent) { 1168 *aOutOffsetInContent = static_cast<int32_t>( 1169 *shrunkenEnd.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets)); 1170 } 1171 if (nsIContent* const previousSiblingOfChildAtOffset = shrunkenEnd.Ref()) { 1172 if (nsIFrame* const childFrame = 1173 previousSiblingOfChildAtOffset->GetPrimaryFrame()) { 1174 const uint32_t offsetInChildFrameContent = 1175 previousSiblingOfChildAtOffset->Length(); 1176 nsIFrame* const childFrameAtOffset = GetChildFrameContainingOffset( 1177 childFrame, offsetInChildFrameContent, CaretAssociationHint::Before); 1178 MOZ_ASSERT(childFrameAtOffset); 1179 // If the child is a non-selectable inline container which has padding or 1180 // border, we want to put the caret after the end edge, but returning the 1181 // frame makes the caller will get rect in its content. Therefore, do not 1182 // return the position in the unselectable container frame. 1183 if (!childFrameAtOffset->IsInlineFrame() || 1184 childFrameAtOffset->IsSelfEmpty()) { 1185 return {childFrameAtOffset, offsetInChildFrameContent}; 1186 } 1187 } 1188 } 1189 nsIContent* const container = 1190 nsIContent::FromNode(shrunkenEnd.GetContainer()); 1191 if (MOZ_UNLIKELY(!container)) { 1192 return {}; 1193 } 1194 nsIFrame* const frame = container->GetPrimaryFrame(); 1195 if (MOZ_UNLIKELY(!frame)) { 1196 return {}; 1197 } 1198 MOZ_ASSERT(frame->IsSelectable()); 1199 const uint32_t offsetInFrameContent = 1200 *shrunkenEnd.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets); 1201 nsIFrame* const frameAtOffset = GetChildFrameContainingOffset( 1202 frame, offsetInFrameContent, CaretAssociationHint::Before); 1203 MOZ_ASSERT(frameAtOffset); 1204 return {frameAtOffset, offsetInFrameContent}; 1205 } 1206 1207 bool AccessibleCaretManager::RestrictCaretDraggingOffsets( 1208 nsIFrame::ContentOffsets& aOffsets) { 1209 if (!mPresShell) { 1210 return false; 1211 } 1212 1213 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection); 1214 1215 nsDirection dir = 1216 mActiveCaret == mCarets.GetFirst() ? eDirPrevious : eDirNext; 1217 nsCOMPtr<nsIContent> content; 1218 int32_t offsetInContent = 0; 1219 const FrameAndOffset frameAndOffset = 1220 dir == eDirNext ? GetFirstVisibleLeafFrameOrUnselectableChildFrame( 1221 *GetSelection()->GetFirstRange(), 1222 getter_AddRefs(content), &offsetInContent) 1223 : GetLastVisibleLeafFrameOrUnselectableChildFrame( 1224 *GetSelection()->GetLastRange(), 1225 getter_AddRefs(content), &offsetInContent); 1226 if (!frameAndOffset) { 1227 return false; 1228 } 1229 1230 // Compare the active caret's new position (aOffsets) to the inactive caret's 1231 // position. 1232 NS_ASSERTION(static_cast<int32_t>(frameAndOffset.mOffsetInFrameContent) >= 0, 1233 "mOffsetInFrameContent should not be negative when casting to " 1234 "signed integer"); 1235 const Maybe<int32_t> cmpToInactiveCaretPos = 1236 nsContentUtils::ComparePoints_AllowNegativeOffsets( 1237 aOffsets.content, aOffsets.StartOffset(), 1238 frameAndOffset.GetFrameContent(), 1239 static_cast<int32_t>(frameAndOffset.mOffsetInFrameContent)); 1240 if (NS_WARN_IF(!cmpToInactiveCaretPos)) { 1241 // Potentially handle this properly when Selection across Shadow DOM 1242 // boundary is implemented 1243 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497). 1244 return false; 1245 } 1246 1247 // Move one character (in the direction of dir) from the inactive caret's 1248 // position. This is the limit for the active caret's new position. 1249 PeekOffsetStruct limit( 1250 eSelectCluster, dir, 1251 static_cast<int32_t>(frameAndOffset.mOffsetInFrameContent), nsPoint(0, 0), 1252 {PeekOffsetOption::JumpLines, PeekOffsetOption::StopAtScroller}); 1253 nsresult rv = frameAndOffset->PeekOffset(&limit); 1254 if (NS_FAILED(rv)) { 1255 limit.mResultContent = content; 1256 limit.mContentOffset = offsetInContent; 1257 } 1258 1259 // Compare the active caret's new position (aOffsets) to the limit. 1260 NS_ASSERTION(limit.mContentOffset >= 0, 1261 "limit.mContentOffset should not be negative"); 1262 const Maybe<int32_t> cmpToLimit = 1263 nsContentUtils::ComparePoints_AllowNegativeOffsets( 1264 aOffsets.content, aOffsets.StartOffset(), limit.mResultContent, 1265 limit.mContentOffset); 1266 if (NS_WARN_IF(!cmpToLimit)) { 1267 // Potentially handle this properly when Selection across Shadow DOM 1268 // boundary is implemented 1269 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497). 1270 return false; 1271 } 1272 1273 auto SetOffsetsToLimit = [&aOffsets, &limit]() { 1274 aOffsets.content = limit.mResultContent; 1275 aOffsets.offset = limit.mContentOffset; 1276 aOffsets.secondaryOffset = limit.mContentOffset; 1277 }; 1278 1279 if (!StaticPrefs:: 1280 layout_accessiblecaret_allow_dragging_across_other_caret()) { 1281 if ((mActiveCaret == mCarets.GetFirst() && *cmpToLimit == 1) || 1282 (mActiveCaret == mCarets.GetSecond() && *cmpToLimit == -1)) { 1283 // The active caret's position is past the limit, which we don't allow 1284 // here. So set it to the limit, resulting in one character being 1285 // selected. 1286 SetOffsetsToLimit(); 1287 } 1288 } else { 1289 switch (*cmpToInactiveCaretPos) { 1290 case 0: 1291 // The active caret's position is the same as the position of the 1292 // inactive caret. So set it to the limit to prevent the selection from 1293 // being collapsed, resulting in one character being selected. 1294 SetOffsetsToLimit(); 1295 break; 1296 case 1: 1297 if (mActiveCaret == mCarets.GetFirst()) { 1298 // First caret was moved across the second caret. After making change 1299 // to the selection, the user will drag the second caret. 1300 mActiveCaret = mCarets.GetSecond(); 1301 } 1302 break; 1303 case -1: 1304 if (mActiveCaret == mCarets.GetSecond()) { 1305 // Second caret was moved across the first caret. After making change 1306 // to the selection, the user will drag the first caret. 1307 mActiveCaret = mCarets.GetFirst(); 1308 } 1309 break; 1310 } 1311 } 1312 1313 return true; 1314 } 1315 1316 bool AccessibleCaretManager::CompareTreePosition(const nsIFrame* aStartFrame, 1317 int32_t aStartOffset, 1318 const nsIFrame* aEndFrame, 1319 int32_t aEndOffset) const { 1320 if (MOZ_UNLIKELY(!aStartFrame || !aStartFrame->GetContent() || !aEndFrame || 1321 !aEndFrame->GetContent())) { 1322 return false; 1323 } 1324 if (aStartFrame->GetContent() == aEndFrame->GetContent()) { 1325 return aStartOffset <= aEndOffset; 1326 } 1327 return nsContentUtils::ComparePoints( 1328 ConstRawRangeBoundary(aStartFrame->GetContent(), 1329 static_cast<uint32_t>(aStartOffset)), 1330 ConstRawRangeBoundary(aEndFrame->GetContent(), 1331 static_cast<uint32_t>(aEndOffset))) 1332 .valueOr(1) <= 0; 1333 } 1334 1335 nsresult AccessibleCaretManager::DragCaretInternal(const nsPoint& aPoint) { 1336 MOZ_ASSERT(mPresShell); 1337 1338 nsIFrame* rootFrame = mPresShell->GetRootFrame(); 1339 MOZ_ASSERT(rootFrame, "We need root frame to compute caret dragging!"); 1340 1341 nsPoint point = AdjustDragBoundary( 1342 nsPoint(aPoint.x, aPoint.y + mOffsetYToCaretLogicalPosition)); 1343 1344 // Find out which content we point to 1345 1346 nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint( 1347 RelativeTo{rootFrame}, point, GetHitTestOptions()); 1348 if (!ptFrame) { 1349 return NS_ERROR_FAILURE; 1350 } 1351 1352 RefPtr<nsFrameSelection> fs = GetFrameSelection(); 1353 MOZ_ASSERT(fs); 1354 1355 nsresult result; 1356 nsIFrame* newFrame = nullptr; 1357 nsPoint newPoint; 1358 nsPoint ptInFrame = point; 1359 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame}, 1360 ptInFrame); 1361 result = fs->ConstrainFrameAndPointToAnchorSubtree(ptFrame, ptInFrame, 1362 &newFrame, newPoint); 1363 if (NS_FAILED(result) || !newFrame) { 1364 return NS_ERROR_FAILURE; 1365 } 1366 1367 if (!newFrame->IsSelectable()) { 1368 return NS_ERROR_FAILURE; 1369 } 1370 1371 nsIFrame::ContentOffsets offsets = newFrame->GetContentOffsetsFromPoint( 1372 newPoint, nsIFrame::IGNORE_NATIVE_ANONYMOUS_SUBTREE); 1373 if (offsets.IsNull()) { 1374 return NS_ERROR_FAILURE; 1375 } 1376 1377 if (GetCaretMode() == CaretMode::Selection && 1378 !RestrictCaretDraggingOffsets(offsets)) { 1379 return NS_ERROR_FAILURE; 1380 } 1381 1382 ClearMaintainedSelection(); 1383 1384 const nsFrameSelection::FocusMode focusMode = 1385 (GetCaretMode() == CaretMode::Selection) 1386 ? nsFrameSelection::FocusMode::kExtendSelection 1387 : nsFrameSelection::FocusMode::kCollapseToNewPoint; 1388 // While dragging the active caret for collapsed selection, we should not 1389 // extend it. However, when crossing an unselectable node, 1390 // GetContentOffsetsFromPoint() above may return the secondary offset. 1391 // Therefore we need to ignore the secondary offset in that case. 1392 int32_t startOffset, endOffset; 1393 if (focusMode == nsFrameSelection::FocusMode::kCollapseToNewPoint) { 1394 startOffset = endOffset = offsets.offset; 1395 } else { 1396 startOffset = offsets.StartOffset(); 1397 endOffset = offsets.EndOffset(); 1398 } 1399 fs->HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */, startOffset, 1400 endOffset, focusMode, offsets.associate); 1401 return NS_OK; 1402 } 1403 1404 // static 1405 nsRect AccessibleCaretManager::GetAllChildFrameRectsUnion(nsIFrame* aFrame) { 1406 nsRect unionRect; 1407 1408 // Drill through scroll frames, we don't want to include scrollbar child 1409 // frames below. 1410 for (nsIFrame* frame = aFrame->GetContentInsertionFrame(); frame; 1411 frame = frame->GetNextContinuation()) { 1412 Maybe<nsRect> childrenRect; 1413 1414 for (const auto& childList : frame->ChildLists()) { 1415 // Loop all children to union their scrollable overflow rect. 1416 for (nsIFrame* child : childList.mList) { 1417 nsRect childRect = child->ScrollableOverflowRectRelativeToSelf(); 1418 nsLayoutUtils::TransformRect(child, frame, childRect); 1419 1420 if (childrenRect) { 1421 // Some frames (e.g. BRFrame or a TextFrame that only contains '\n') 1422 // can have a positive block size but a zero inline size. Using 1423 // UnionEdges ensures these dimensions are properly included in 1424 // childrenRect. 1425 *childrenRect = childrenRect->UnionEdges(childRect); 1426 } else { 1427 childrenRect.emplace(childRect); 1428 } 1429 } 1430 } 1431 1432 if (childrenRect) { 1433 if (frame != aFrame) { 1434 nsLayoutUtils::TransformRect(frame, aFrame, *childrenRect); 1435 } 1436 unionRect = unionRect.Union(*childrenRect); 1437 } 1438 } 1439 1440 return unionRect; 1441 } 1442 1443 nsPoint AccessibleCaretManager::AdjustDragBoundary( 1444 const nsPoint& aPoint) const { 1445 nsPoint adjustedPoint = aPoint; 1446 1447 auto frameData = 1448 nsCaret::GetFrameAndOffset(nsCaret::CaretPositionFor(GetSelection())); 1449 Element* editingHost = GetEditingHostForFrame(frameData.mFrame); 1450 1451 if (editingHost) { 1452 nsIFrame* editingHostFrame = editingHost->GetPrimaryFrame(); 1453 if (editingHostFrame) { 1454 nsRect boundary = 1455 AccessibleCaretManager::GetAllChildFrameRectsUnion(editingHostFrame); 1456 nsLayoutUtils::TransformRect(editingHostFrame, mPresShell->GetRootFrame(), 1457 boundary); 1458 1459 // Shrink the rect to make sure we never hit the boundary. 1460 boundary.Deflate(kBoundaryAppUnits); 1461 1462 adjustedPoint = boundary.ClampPoint(adjustedPoint); 1463 } 1464 } 1465 1466 if (GetCaretMode() == CaretMode::Selection && 1467 !StaticPrefs:: 1468 layout_accessiblecaret_allow_dragging_across_other_caret()) { 1469 // Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt 1470 // mode when a caret is being dragged surpass the other caret. 1471 // 1472 // For example, when dragging the second caret, the horizontal boundary 1473 // (lower bound) of its Y-coordinate is the logical position of the first 1474 // caret. Likewise, when dragging the first caret, the horizontal boundary 1475 // (upper bound) of its Y-coordinate is the logical position of the second 1476 // caret. 1477 if (mActiveCaret == mCarets.GetFirst()) { 1478 nscoord dragDownBoundaryY = mCarets.GetSecond()->LogicalPosition().y; 1479 if (dragDownBoundaryY > 0 && adjustedPoint.y > dragDownBoundaryY) { 1480 adjustedPoint.y = dragDownBoundaryY; 1481 } 1482 } else { 1483 nscoord dragUpBoundaryY = mCarets.GetFirst()->LogicalPosition().y; 1484 if (adjustedPoint.y < dragUpBoundaryY) { 1485 adjustedPoint.y = dragUpBoundaryY; 1486 } 1487 } 1488 } 1489 1490 return adjustedPoint; 1491 } 1492 1493 void AccessibleCaretManager::StartSelectionAutoScrollTimer( 1494 const nsPoint& aPoint) const { 1495 Selection* selection = GetSelection(); 1496 MOZ_ASSERT(selection); 1497 1498 nsIFrame* anchorFrame = selection->GetPrimaryFrameForAnchorNode(); 1499 if (!anchorFrame) { 1500 return; 1501 } 1502 1503 ScrollContainerFrame* scrollContainerFrame = 1504 nsLayoutUtils::GetNearestScrollContainerFrame( 1505 anchorFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC | 1506 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN); 1507 if (!scrollContainerFrame) { 1508 return; 1509 } 1510 1511 nsIFrame* capturingFrame = scrollContainerFrame->GetScrolledFrame(); 1512 if (!capturingFrame) { 1513 return; 1514 } 1515 1516 nsIFrame* rootFrame = mPresShell->GetRootFrame(); 1517 MOZ_ASSERT(rootFrame); 1518 nsPoint ptInScrolled = aPoint; 1519 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, 1520 RelativeTo{capturingFrame}, ptInScrolled); 1521 1522 RefPtr<nsFrameSelection> fs = GetFrameSelection(); 1523 MOZ_ASSERT(fs); 1524 fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, kAutoScrollTimerDelay); 1525 } 1526 1527 void AccessibleCaretManager::StopSelectionAutoScrollTimer() const { 1528 RefPtr<nsFrameSelection> fs = GetFrameSelection(); 1529 MOZ_ASSERT(fs); 1530 fs->StopAutoScrollTimer(); 1531 } 1532 1533 void AccessibleCaretManager::DispatchCaretStateChangedEvent( 1534 CaretChangedReason aReason, const nsPoint* aPoint) { 1535 if (MaybeFlushLayout() == Terminated::Yes) { 1536 return; 1537 } 1538 1539 const Selection* sel = GetSelection(); 1540 if (!sel) { 1541 return; 1542 } 1543 1544 Document* doc = mPresShell->GetDocument(); 1545 MOZ_ASSERT(doc); 1546 1547 CaretStateChangedEventInit init; 1548 init.mBubbles = true; 1549 1550 const nsRange* range = sel->GetAnchorFocusRange(); 1551 nsINode* commonAncestorNode = nullptr; 1552 if (range) { 1553 commonAncestorNode = range->GetClosestCommonInclusiveAncestor(); 1554 } 1555 1556 if (!commonAncestorNode) { 1557 commonAncestorNode = sel->GetFrameSelection()->GetAncestorLimiter(); 1558 } 1559 1560 auto domRect = MakeRefPtr<DOMRect>(ToSupports(doc)); 1561 nsRect rect = nsLayoutUtils::GetSelectionBoundingRect(sel); 1562 1563 nsIFrame* commonAncestorFrame = nullptr; 1564 nsIFrame* rootFrame = mPresShell->GetRootFrame(); 1565 1566 if (commonAncestorNode && commonAncestorNode->IsContent()) { 1567 commonAncestorFrame = commonAncestorNode->AsContent()->GetPrimaryFrame(); 1568 } 1569 1570 if (commonAncestorFrame && rootFrame) { 1571 nsLayoutUtils::TransformRect(rootFrame, commonAncestorFrame, rect); 1572 nsRect clampedRect = 1573 nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame, rect); 1574 nsLayoutUtils::TransformRect(commonAncestorFrame, rootFrame, clampedRect); 1575 rect = clampedRect; 1576 init.mSelectionVisible = !clampedRect.IsEmpty(); 1577 } else { 1578 init.mSelectionVisible = true; 1579 } 1580 1581 domRect->SetLayoutRect(rect); 1582 1583 // Send isEditable info w/ event detail. This info can help determine 1584 // whether to show cut command on selection dialog or not. 1585 init.mSelectionEditable = GetEditingHostForFrame(commonAncestorFrame); 1586 1587 init.mBoundingClientRect = domRect; 1588 init.mReason = aReason; 1589 init.mCollapsed = sel->IsCollapsed(); 1590 init.mCaretVisible = mCarets.HasLogicallyVisibleCaret(); 1591 init.mCaretVisuallyVisible = mCarets.HasVisuallyVisibleCaret(); 1592 init.mSelectedTextContent = StringifiedSelection(); 1593 1594 if (aPoint) { 1595 CSSIntPoint pt = CSSPixel::FromAppUnitsRounded(*aPoint); 1596 init.mClientX = pt.x; 1597 init.mClientY = pt.y; 1598 } 1599 1600 RefPtr<CaretStateChangedEvent> event = CaretStateChangedEvent::Constructor( 1601 doc, u"mozcaretstatechanged"_ns, init); 1602 event->SetTrusted(true); 1603 1604 AC_LOG("%s: reason %" PRIu32 ", collapsed %d, caretVisible %" PRIu32, 1605 __FUNCTION__, static_cast<uint32_t>(init.mReason), init.mCollapsed, 1606 static_cast<uint32_t>(init.mCaretVisible)); 1607 1608 (new AsyncEventDispatcher(doc, event.forget(), ChromeOnlyDispatch::eYes)) 1609 ->PostDOMEvent(); 1610 } 1611 1612 AccessibleCaretManager::Carets::Carets(UniquePtr<AccessibleCaret> aFirst, 1613 UniquePtr<AccessibleCaret> aSecond) 1614 : mFirst{std::move(aFirst)}, mSecond{std::move(aSecond)} {} 1615 1616 } // namespace mozilla