Line data Source code
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 "AccessibleCaret.h"
10 : #include "AccessibleCaretEventHub.h"
11 : #include "AccessibleCaretLogger.h"
12 : #include "mozilla/AsyncEventDispatcher.h"
13 : #include "mozilla/dom/Element.h"
14 : #include "mozilla/dom/Selection.h"
15 : #include "mozilla/dom/TreeWalker.h"
16 : #include "mozilla/IMEStateManager.h"
17 : #include "mozilla/IntegerPrintfMacros.h"
18 : #include "mozilla/Preferences.h"
19 : #include "nsCaret.h"
20 : #include "nsContainerFrame.h"
21 : #include "nsContentUtils.h"
22 : #include "nsFocusManager.h"
23 : #include "nsFrame.h"
24 : #include "nsFrameSelection.h"
25 : #include "nsGenericHTMLElement.h"
26 : #include "nsIHapticFeedback.h"
27 : #ifdef MOZ_WIDGET_ANDROID
28 : #include "nsWindow.h"
29 : #endif
30 :
31 : namespace mozilla {
32 :
33 : #undef AC_LOG
34 : #define AC_LOG(message, ...) \
35 : AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
36 :
37 : #undef AC_LOGV
38 : #define AC_LOGV(message, ...) \
39 : AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
40 :
41 : using namespace dom;
42 : using Appearance = AccessibleCaret::Appearance;
43 : using PositionChangedResult = AccessibleCaret::PositionChangedResult;
44 :
45 : #define AC_PROCESS_ENUM_TO_STREAM(e) case(e): aStream << #e; break;
46 : std::ostream&
47 0 : operator<<(std::ostream& aStream,
48 : const AccessibleCaretManager::CaretMode& aCaretMode)
49 : {
50 : using CaretMode = AccessibleCaretManager::CaretMode;
51 0 : switch (aCaretMode) {
52 0 : AC_PROCESS_ENUM_TO_STREAM(CaretMode::None);
53 0 : AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor);
54 0 : AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection);
55 : }
56 0 : return aStream;
57 : }
58 :
59 0 : std::ostream& operator<<(std::ostream& aStream,
60 : const AccessibleCaretManager::UpdateCaretsHint& aHint)
61 : {
62 : using UpdateCaretsHint = AccessibleCaretManager::UpdateCaretsHint;
63 0 : switch (aHint) {
64 0 : AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default);
65 0 : AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance);
66 0 : AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent);
67 : }
68 0 : return aStream;
69 : }
70 : #undef AC_PROCESS_ENUM_TO_STREAM
71 :
72 : /* static */ bool
73 : AccessibleCaretManager::sSelectionBarEnabled = false;
74 : /* static */ bool
75 : AccessibleCaretManager::sCaretShownWhenLongTappingOnEmptyContent = false;
76 : /* static */ bool
77 : AccessibleCaretManager::sCaretsAlwaysTilt = false;
78 : /* static */ bool
79 : AccessibleCaretManager::sCaretsAlwaysShowWhenScrolling = true;
80 : /* static */ bool
81 : AccessibleCaretManager::sCaretsScriptUpdates = false;
82 : /* static */ bool
83 : AccessibleCaretManager::sCaretsAllowDraggingAcrossOtherCaret = true;
84 : /* static */ bool
85 : AccessibleCaretManager::sHapticFeedback = false;
86 : /* static */ bool
87 : AccessibleCaretManager::sExtendSelectionForPhoneNumber = false;
88 : /* static */ bool
89 : AccessibleCaretManager::sHideCaretsForMouseInput = true;
90 :
91 0 : AccessibleCaretManager::AccessibleCaretManager(nsIPresShell* aPresShell)
92 0 : : mPresShell(aPresShell)
93 : {
94 0 : if (!mPresShell) {
95 0 : return;
96 : }
97 :
98 0 : mFirstCaret = MakeUnique<AccessibleCaret>(mPresShell);
99 0 : mSecondCaret = MakeUnique<AccessibleCaret>(mPresShell);
100 :
101 : static bool addedPrefs = false;
102 0 : if (!addedPrefs) {
103 : Preferences::AddBoolVarCache(&sSelectionBarEnabled,
104 0 : "layout.accessiblecaret.bar.enabled");
105 : Preferences::AddBoolVarCache(&sCaretShownWhenLongTappingOnEmptyContent,
106 0 : "layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content");
107 : Preferences::AddBoolVarCache(&sCaretsAlwaysTilt,
108 0 : "layout.accessiblecaret.always_tilt");
109 : Preferences::AddBoolVarCache(&sCaretsAlwaysShowWhenScrolling,
110 0 : "layout.accessiblecaret.always_show_when_scrolling", true);
111 : Preferences::AddBoolVarCache(&sCaretsScriptUpdates,
112 0 : "layout.accessiblecaret.allow_script_change_updates");
113 : Preferences::AddBoolVarCache(&sCaretsAllowDraggingAcrossOtherCaret,
114 0 : "layout.accessiblecaret.allow_dragging_across_other_caret", true);
115 : Preferences::AddBoolVarCache(&sHapticFeedback,
116 0 : "layout.accessiblecaret.hapticfeedback");
117 : Preferences::AddBoolVarCache(&sExtendSelectionForPhoneNumber,
118 0 : "layout.accessiblecaret.extend_selection_for_phone_number");
119 : Preferences::AddBoolVarCache(&sHideCaretsForMouseInput,
120 0 : "layout.accessiblecaret.hide_carets_for_mouse_input");
121 0 : addedPrefs = true;
122 : }
123 : }
124 :
125 0 : AccessibleCaretManager::~AccessibleCaretManager()
126 : {
127 0 : }
128 :
129 : void
130 0 : AccessibleCaretManager::Terminate()
131 : {
132 0 : mFirstCaret = nullptr;
133 0 : mSecondCaret = nullptr;
134 0 : mActiveCaret = nullptr;
135 0 : mPresShell = nullptr;
136 0 : }
137 :
138 : nsresult
139 0 : AccessibleCaretManager::OnSelectionChanged(nsIDOMDocument* aDoc,
140 : nsISelection* aSel, int16_t aReason)
141 : {
142 0 : Selection* selection = GetSelection();
143 0 : AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__,
144 : aSel, selection, aReason);
145 0 : if (aSel != selection) {
146 0 : return NS_OK;
147 : }
148 :
149 : // eSetSelection events from the Fennec widget IME can be generated
150 : // by autoSuggest / autoCorrect composition changes, or by TYPE_REPLACE_TEXT
151 : // actions, either positioning cursor for text insert, or selecting
152 : // text-to-be-replaced. None should affect AccessibleCaret visibility.
153 0 : if (aReason & nsISelectionListener::IME_REASON) {
154 0 : return NS_OK;
155 : }
156 :
157 : // Move the cursor by Javascript / or unknown internal.
158 0 : if (aReason == nsISelectionListener::NO_REASON) {
159 : // Update visible carets, if javascript changes are allowed.
160 0 : if (sCaretsScriptUpdates &&
161 0 : (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible())) {
162 0 : UpdateCarets();
163 0 : return NS_OK;
164 : }
165 : // Default for NO_REASON is to make hidden.
166 0 : HideCarets();
167 0 : return NS_OK;
168 : }
169 :
170 : // Move cursor by keyboard.
171 0 : if (aReason & nsISelectionListener::KEYPRESS_REASON) {
172 0 : HideCarets();
173 0 : return NS_OK;
174 : }
175 :
176 : // OnBlur() might be called between mouse down and mouse up, so we hide carets
177 : // upon mouse down anyway, and update carets upon mouse up.
178 0 : if (aReason & nsISelectionListener::MOUSEDOWN_REASON) {
179 0 : HideCarets();
180 0 : return NS_OK;
181 : }
182 :
183 : // Range will collapse after cutting or copying text.
184 0 : if (aReason & (nsISelectionListener::COLLAPSETOSTART_REASON |
185 : nsISelectionListener::COLLAPSETOEND_REASON)) {
186 0 : HideCarets();
187 0 : return NS_OK;
188 : }
189 :
190 : // For mouse input we don't want to show the carets.
191 0 : if (sHideCaretsForMouseInput &&
192 0 : mLastInputSource == nsIDOMMouseEvent::MOZ_SOURCE_MOUSE) {
193 0 : HideCarets();
194 0 : return NS_OK;
195 : }
196 :
197 : // When we want to hide the carets for mouse input, hide them for select
198 : // all action fired by keyboard as well.
199 0 : if (sHideCaretsForMouseInput &&
200 0 : mLastInputSource == nsIDOMMouseEvent::MOZ_SOURCE_KEYBOARD &&
201 0 : (aReason & nsISelectionListener::SELECTALL_REASON)) {
202 0 : HideCarets();
203 0 : return NS_OK;
204 : }
205 :
206 0 : UpdateCarets();
207 0 : return NS_OK;
208 : }
209 :
210 : void
211 0 : AccessibleCaretManager::HideCarets()
212 : {
213 0 : if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
214 0 : AC_LOG("%s", __FUNCTION__);
215 0 : mFirstCaret->SetAppearance(Appearance::None);
216 0 : mSecondCaret->SetAppearance(Appearance::None);
217 0 : DispatchCaretStateChangedEvent(CaretChangedReason::Visibilitychange);
218 : }
219 0 : }
220 :
221 : void
222 0 : AccessibleCaretManager::UpdateCarets(UpdateCaretsHintSet aHint)
223 : {
224 0 : FlushLayout();
225 0 : if (IsTerminated()) {
226 0 : return;
227 : }
228 :
229 0 : mLastUpdateCaretMode = GetCaretMode();
230 :
231 0 : switch (mLastUpdateCaretMode) {
232 : case CaretMode::None:
233 0 : HideCarets();
234 0 : break;
235 : case CaretMode::Cursor:
236 0 : UpdateCaretsForCursorMode(aHint);
237 0 : break;
238 : case CaretMode::Selection:
239 0 : UpdateCaretsForSelectionMode(aHint);
240 0 : break;
241 : }
242 : }
243 :
244 : bool
245 0 : AccessibleCaretManager::IsCaretDisplayableInCursorMode(nsIFrame** aOutFrame,
246 : int32_t* aOutOffset) const
247 : {
248 0 : RefPtr<nsCaret> caret = mPresShell->GetCaret();
249 0 : if (!caret || !caret->IsVisible()) {
250 0 : return false;
251 : }
252 :
253 0 : int32_t offset = 0;
254 0 : nsIFrame* frame = nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset);
255 :
256 0 : if (!frame) {
257 0 : return false;
258 : }
259 :
260 0 : if (!GetEditingHostForFrame(frame)) {
261 0 : return false;
262 : }
263 :
264 0 : if (aOutFrame) {
265 0 : *aOutFrame = frame;
266 : }
267 :
268 0 : if (aOutOffset) {
269 0 : *aOutOffset = offset;
270 : }
271 :
272 0 : return true;
273 : }
274 :
275 : bool
276 0 : AccessibleCaretManager::HasNonEmptyTextContent(nsINode* aNode) const
277 : {
278 : return nsContentUtils::HasNonEmptyTextContent(
279 0 : aNode, nsContentUtils::eRecurseIntoChildren);
280 : }
281 :
282 : void
283 0 : AccessibleCaretManager::UpdateCaretsForCursorMode(UpdateCaretsHintSet aHints)
284 : {
285 0 : AC_LOG("%s, selection: %p", __FUNCTION__, GetSelection());
286 :
287 0 : int32_t offset = 0;
288 0 : nsIFrame* frame = nullptr;
289 0 : if (!IsCaretDisplayableInCursorMode(&frame, &offset)) {
290 0 : HideCarets();
291 0 : return;
292 : }
293 :
294 0 : PositionChangedResult result = mFirstCaret->SetPosition(frame, offset);
295 :
296 0 : switch (result) {
297 : case PositionChangedResult::NotChanged:
298 : case PositionChangedResult::Changed:
299 0 : if (aHints == UpdateCaretsHint::Default) {
300 0 : if (HasNonEmptyTextContent(GetEditingHostForFrame(frame))) {
301 0 : mFirstCaret->SetAppearance(Appearance::Normal);
302 0 : } else if (sCaretShownWhenLongTappingOnEmptyContent) {
303 0 : if (mFirstCaret->IsLogicallyVisible()) {
304 : // Possible cases are: 1) SelectWordOrShortcut() sets the
305 : // appearance to Normal. 2) When the caret is out of viewport and
306 : // now scrolling into viewport, it has appearance NormalNotShown.
307 0 : mFirstCaret->SetAppearance(Appearance::Normal);
308 : } else {
309 : // Possible cases are: a) Single tap on current empty content;
310 : // OnSelectionChanged() sets the appearance to None due to
311 : // MOUSEDOWN_REASON. b) Single tap on other empty content;
312 : // OnBlur() sets the appearance to None.
313 : //
314 : // Do nothing to make the appearance remains None so that it can
315 : // be distinguished from case 2). Also do not set the appearance
316 : // to NormalNotShown here like the default update behavior.
317 : }
318 : } else {
319 0 : mFirstCaret->SetAppearance(Appearance::NormalNotShown);
320 : }
321 0 : } else if (aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
322 : // Do nothing to preserve the appearance of the caret set by the
323 : // caller.
324 : }
325 0 : break;
326 :
327 : case PositionChangedResult::Invisible:
328 0 : mFirstCaret->SetAppearance(Appearance::NormalNotShown);
329 0 : break;
330 : }
331 :
332 0 : mFirstCaret->SetSelectionBarEnabled(false);
333 0 : mSecondCaret->SetAppearance(Appearance::None);
334 :
335 0 : if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) &&
336 0 : !mActiveCaret) {
337 0 : DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
338 : }
339 : }
340 :
341 : void
342 0 : AccessibleCaretManager::UpdateCaretsForSelectionMode(UpdateCaretsHintSet aHints)
343 : {
344 0 : AC_LOG("%s: selection: %p", __FUNCTION__, GetSelection());
345 :
346 0 : int32_t startOffset = 0;
347 : nsIFrame* startFrame =
348 0 : GetFrameForFirstRangeStartOrLastRangeEnd(eDirNext, &startOffset);
349 :
350 0 : int32_t endOffset = 0;
351 : nsIFrame* endFrame =
352 0 : GetFrameForFirstRangeStartOrLastRangeEnd(eDirPrevious, &endOffset);
353 :
354 0 : if (!CompareTreePosition(startFrame, endFrame)) {
355 : // XXX: Do we really have to hide carets if this condition isn't satisfied?
356 0 : HideCarets();
357 0 : return;
358 : }
359 :
360 : auto updateSingleCaret = [aHints](AccessibleCaret* aCaret, nsIFrame* aFrame,
361 0 : int32_t aOffset) -> PositionChangedResult
362 : {
363 0 : PositionChangedResult result = aCaret->SetPosition(aFrame, aOffset);
364 0 : aCaret->SetSelectionBarEnabled(sSelectionBarEnabled);
365 :
366 0 : switch (result) {
367 : case PositionChangedResult::NotChanged:
368 : case PositionChangedResult::Changed:
369 0 : if (aHints == UpdateCaretsHint::Default) {
370 0 : aCaret->SetAppearance(Appearance::Normal);
371 0 : } else if (aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
372 : // Do nothing to preserve the appearance of the caret set by the
373 : // caller.
374 : }
375 0 : break;
376 :
377 : case PositionChangedResult::Invisible:
378 0 : aCaret->SetAppearance(Appearance::NormalNotShown);
379 0 : break;
380 : }
381 0 : return result;
382 0 : };
383 :
384 : PositionChangedResult firstCaretResult =
385 0 : updateSingleCaret(mFirstCaret.get(), startFrame, startOffset);
386 : PositionChangedResult secondCaretResult =
387 0 : updateSingleCaret(mSecondCaret.get(), endFrame, endOffset);
388 :
389 0 : if (firstCaretResult == PositionChangedResult::Changed ||
390 : secondCaretResult == PositionChangedResult::Changed) {
391 : // Flush layout to make the carets intersection correct.
392 0 : FlushLayout();
393 0 : if (IsTerminated()) {
394 0 : return;
395 : }
396 : }
397 :
398 0 : if (aHints == UpdateCaretsHint::Default) {
399 : // Only check for tilt carets with default update hint. Otherwise we might
400 : // override the appearance set by the caller.
401 0 : if (sCaretsAlwaysTilt) {
402 0 : UpdateCaretsForAlwaysTilt(startFrame, endFrame);
403 : } else {
404 0 : UpdateCaretsForOverlappingTilt();
405 : }
406 : }
407 :
408 0 : if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) &&
409 0 : !mActiveCaret) {
410 0 : DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
411 : }
412 : }
413 :
414 : bool
415 0 : AccessibleCaretManager::UpdateCaretsForOverlappingTilt()
416 : {
417 0 : if (!mFirstCaret->IsVisuallyVisible() || !mSecondCaret->IsVisuallyVisible()) {
418 0 : return false;
419 : }
420 :
421 0 : if (!mFirstCaret->Intersects(*mSecondCaret)) {
422 0 : mFirstCaret->SetAppearance(Appearance::Normal);
423 0 : mSecondCaret->SetAppearance(Appearance::Normal);
424 0 : return false;
425 : }
426 :
427 0 : if (mFirstCaret->LogicalPosition().x <=
428 0 : mSecondCaret->LogicalPosition().x) {
429 0 : mFirstCaret->SetAppearance(Appearance::Left);
430 0 : mSecondCaret->SetAppearance(Appearance::Right);
431 : } else {
432 0 : mFirstCaret->SetAppearance(Appearance::Right);
433 0 : mSecondCaret->SetAppearance(Appearance::Left);
434 : }
435 :
436 0 : return true;
437 : }
438 :
439 : void
440 0 : AccessibleCaretManager::UpdateCaretsForAlwaysTilt(nsIFrame* aStartFrame,
441 : nsIFrame* aEndFrame)
442 : {
443 : // When a short LTR word in RTL environment is selected, the two carets
444 : // tilted inward might be overlapped. Make them tilt outward.
445 0 : if (UpdateCaretsForOverlappingTilt()) {
446 0 : return;
447 : }
448 :
449 0 : if (mFirstCaret->IsVisuallyVisible()) {
450 0 : auto startFrameWritingMode = aStartFrame->GetWritingMode();
451 0 : mFirstCaret->SetAppearance(startFrameWritingMode.IsBidiLTR() ?
452 0 : Appearance::Left : Appearance::Right);
453 : }
454 0 : if (mSecondCaret->IsVisuallyVisible()) {
455 0 : auto endFrameWritingMode = aEndFrame->GetWritingMode();
456 0 : mSecondCaret->SetAppearance(endFrameWritingMode.IsBidiLTR() ?
457 0 : Appearance::Right : Appearance::Left);
458 : }
459 : }
460 :
461 : void
462 0 : AccessibleCaretManager::ProvideHapticFeedback()
463 : {
464 0 : if (sHapticFeedback) {
465 : nsCOMPtr<nsIHapticFeedback> haptic =
466 0 : do_GetService("@mozilla.org/widget/hapticfeedback;1");
467 0 : haptic->PerformSimpleAction(haptic->LongPress);
468 : }
469 0 : }
470 :
471 : nsresult
472 0 : AccessibleCaretManager::PressCaret(const nsPoint& aPoint,
473 : EventClassID aEventClass)
474 : {
475 0 : nsresult rv = NS_ERROR_FAILURE;
476 :
477 0 : MOZ_ASSERT(aEventClass == eMouseEventClass || aEventClass == eTouchEventClass,
478 : "Unexpected event class!");
479 :
480 : using TouchArea = AccessibleCaret::TouchArea;
481 : TouchArea touchArea =
482 0 : aEventClass == eMouseEventClass ? TouchArea::CaretImage : TouchArea::Full;
483 :
484 0 : if (mFirstCaret->Contains(aPoint, touchArea)) {
485 0 : mActiveCaret = mFirstCaret.get();
486 0 : SetSelectionDirection(eDirPrevious);
487 0 : } else if (mSecondCaret->Contains(aPoint, touchArea)) {
488 0 : mActiveCaret = mSecondCaret.get();
489 0 : SetSelectionDirection(eDirNext);
490 : }
491 :
492 0 : if (mActiveCaret) {
493 0 : mOffsetYToCaretLogicalPosition =
494 0 : mActiveCaret->LogicalPosition().y - aPoint.y;
495 0 : SetSelectionDragState(true);
496 0 : DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret);
497 0 : rv = NS_OK;
498 : }
499 :
500 0 : return rv;
501 : }
502 :
503 : nsresult
504 0 : AccessibleCaretManager::DragCaret(const nsPoint& aPoint)
505 : {
506 0 : MOZ_ASSERT(mActiveCaret);
507 0 : MOZ_ASSERT(GetCaretMode() != CaretMode::None);
508 :
509 0 : nsPoint point(aPoint.x, aPoint.y + mOffsetYToCaretLogicalPosition);
510 0 : DragCaretInternal(point);
511 0 : UpdateCarets();
512 0 : return NS_OK;
513 : }
514 :
515 : nsresult
516 0 : AccessibleCaretManager::ReleaseCaret()
517 : {
518 0 : MOZ_ASSERT(mActiveCaret);
519 :
520 0 : mActiveCaret = nullptr;
521 0 : SetSelectionDragState(false);
522 0 : DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret);
523 0 : return NS_OK;
524 : }
525 :
526 : nsresult
527 0 : AccessibleCaretManager::TapCaret(const nsPoint& aPoint)
528 : {
529 0 : MOZ_ASSERT(GetCaretMode() != CaretMode::None);
530 :
531 0 : nsresult rv = NS_ERROR_FAILURE;
532 :
533 0 : if (GetCaretMode() == CaretMode::Cursor) {
534 0 : DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret);
535 0 : rv = NS_OK;
536 : }
537 :
538 0 : return rv;
539 : }
540 :
541 : nsresult
542 0 : AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint)
543 : {
544 0 : auto UpdateCaretsWithHapticFeedback = [this] {
545 0 : UpdateCarets();
546 0 : ProvideHapticFeedback();
547 0 : };
548 :
549 : // If the long-tap is landing on a pre-existing selection, don't replace
550 : // it with a new one. Instead just return and let the context menu pop up
551 : // on the pre-existing selection.
552 0 : if (GetCaretMode() == CaretMode::Selection &&
553 0 : GetSelection()->ContainsPoint(aPoint)) {
554 0 : AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__);
555 0 : UpdateCaretsWithHapticFeedback();
556 0 : return NS_OK;
557 : }
558 :
559 0 : if (!mPresShell) {
560 0 : return NS_ERROR_UNEXPECTED;
561 : }
562 :
563 0 : nsIFrame* rootFrame = mPresShell->GetRootFrame();
564 0 : if (!rootFrame) {
565 0 : return NS_ERROR_NOT_AVAILABLE;
566 : }
567 :
568 : // Find the frame under point.
569 : AutoWeakFrame ptFrame = nsLayoutUtils::GetFrameForPoint(rootFrame, aPoint,
570 0 : nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
571 0 : if (!ptFrame.IsAlive()) {
572 0 : return NS_ERROR_FAILURE;
573 : }
574 :
575 0 : nsIFrame* focusableFrame = GetFocusableFrame(ptFrame);
576 :
577 : #ifdef DEBUG_FRAME_DUMP
578 0 : AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__, ptFrame->ListTag().get(),
579 : aPoint.x, aPoint.y);
580 0 : AC_LOG("%s: Found %s focusable", __FUNCTION__,
581 : focusableFrame ? focusableFrame->ListTag().get() : "no frame");
582 : #endif
583 :
584 : // Get ptInFrame here so that we don't need to check whether rootFrame is
585 : // alive later. Note that if ptFrame is being moved by
586 : // IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below,
587 : // something under the original point will be selected, which may not be the
588 : // original text the user wants to select.
589 0 : nsPoint ptInFrame = aPoint;
590 0 : nsLayoutUtils::TransformPoint(rootFrame, ptFrame, ptInFrame);
591 :
592 : // Firstly check long press on an empty editable content.
593 0 : Element* newFocusEditingHost = GetEditingHostForFrame(ptFrame);
594 0 : if (focusableFrame && newFocusEditingHost &&
595 0 : !HasNonEmptyTextContent(newFocusEditingHost)) {
596 0 : ChangeFocusToOrClearOldFocus(focusableFrame);
597 :
598 0 : if (sCaretShownWhenLongTappingOnEmptyContent) {
599 0 : mFirstCaret->SetAppearance(Appearance::Normal);
600 : }
601 : // We need to update carets to get correct information before dispatching
602 : // CaretStateChangedEvent.
603 0 : UpdateCaretsWithHapticFeedback();
604 0 : DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent);
605 0 : return NS_OK;
606 : }
607 :
608 0 : bool selectable = ptFrame->IsSelectable(nullptr);
609 :
610 : #ifdef DEBUG_FRAME_DUMP
611 0 : AC_LOG("%s: %s %s selectable.", __FUNCTION__, ptFrame->ListTag().get(),
612 : selectable ? "is" : "is NOT");
613 : #endif
614 :
615 0 : if (!selectable) {
616 0 : return NS_ERROR_FAILURE;
617 : }
618 :
619 : // Commit the composition string of the old editable focus element (if there
620 : // is any) before changing the focus.
621 0 : IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION,
622 0 : mPresShell->GetPresContext());
623 0 : if (!ptFrame.IsAlive()) {
624 : // Cannot continue because ptFrame died.
625 0 : return NS_ERROR_FAILURE;
626 : }
627 :
628 : // ptFrame is selectable. Now change the focus.
629 0 : ChangeFocusToOrClearOldFocus(focusableFrame);
630 0 : if (!ptFrame.IsAlive()) {
631 : // Cannot continue because ptFrame died.
632 0 : return NS_ERROR_FAILURE;
633 : }
634 :
635 : // Then try select a word under point.
636 0 : nsresult rv = SelectWord(ptFrame, ptInFrame);
637 0 : UpdateCaretsWithHapticFeedback();
638 :
639 0 : return rv;
640 : }
641 :
642 : void
643 0 : AccessibleCaretManager::OnScrollStart()
644 : {
645 0 : AC_LOG("%s", __FUNCTION__);
646 :
647 0 : mIsScrollStarted = true;
648 :
649 0 : if (!sCaretsAlwaysShowWhenScrolling) {
650 : // Backup the appearance so that we can restore them after the scrolling
651 : // ends.
652 0 : mFirstCaretAppearanceOnScrollStart = mFirstCaret->GetAppearance();
653 0 : mSecondCaretAppearanceOnScrollStart = mSecondCaret->GetAppearance();
654 0 : HideCarets();
655 0 : return;
656 : }
657 :
658 0 : if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
659 : // Dispatch the event only if one of the carets is logically visible like in
660 : // HideCarets().
661 0 : DispatchCaretStateChangedEvent(CaretChangedReason::Scroll);
662 : }
663 : }
664 :
665 : void
666 0 : AccessibleCaretManager::OnScrollEnd()
667 : {
668 0 : if (mLastUpdateCaretMode != GetCaretMode()) {
669 0 : return;
670 : }
671 :
672 0 : mIsScrollStarted = false;
673 :
674 0 : if (!sCaretsAlwaysShowWhenScrolling) {
675 : // Restore the appearance which is saved before the scrolling is started.
676 0 : mFirstCaret->SetAppearance(mFirstCaretAppearanceOnScrollStart);
677 0 : mSecondCaret->SetAppearance(mSecondCaretAppearanceOnScrollStart);
678 : }
679 :
680 0 : if (GetCaretMode() == CaretMode::Cursor) {
681 0 : if (!mFirstCaret->IsLogicallyVisible()) {
682 : // If the caret is hidden (Appearance::None) due to blur, no
683 : // need to update it.
684 0 : return;
685 : }
686 : }
687 :
688 : // For mouse input we don't want to show the carets.
689 0 : if (sHideCaretsForMouseInput &&
690 0 : mLastInputSource == nsIDOMMouseEvent::MOZ_SOURCE_MOUSE) {
691 0 : AC_LOG("%s: HideCarets()", __FUNCTION__);
692 0 : HideCarets();
693 0 : return;
694 : }
695 :
696 0 : AC_LOG("%s: UpdateCarets()", __FUNCTION__);
697 0 : UpdateCarets();
698 : }
699 :
700 : void
701 0 : AccessibleCaretManager::OnScrollPositionChanged()
702 : {
703 0 : if (mLastUpdateCaretMode != GetCaretMode()) {
704 0 : return;
705 : }
706 :
707 0 : if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
708 0 : if (mIsScrollStarted) {
709 : // We don't want extra CaretStateChangedEvents dispatched when user is
710 : // scrolling the page.
711 0 : AC_LOG("%s: UpdateCarets(RespectOldAppearance | DispatchNoEvent)",
712 : __FUNCTION__);
713 0 : UpdateCarets({ UpdateCaretsHint::RespectOldAppearance,
714 0 : UpdateCaretsHint::DispatchNoEvent });
715 : } else {
716 0 : AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
717 0 : UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
718 : }
719 : }
720 : }
721 :
722 : void
723 0 : AccessibleCaretManager::OnReflow()
724 : {
725 0 : if (mLastUpdateCaretMode != GetCaretMode()) {
726 0 : return;
727 : }
728 :
729 0 : if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
730 0 : AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
731 0 : UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
732 : }
733 : }
734 :
735 : void
736 0 : AccessibleCaretManager::OnBlur()
737 : {
738 0 : AC_LOG("%s: HideCarets()", __FUNCTION__);
739 0 : HideCarets();
740 0 : }
741 :
742 : void
743 0 : AccessibleCaretManager::OnKeyboardEvent()
744 : {
745 0 : if (GetCaretMode() == CaretMode::Cursor) {
746 0 : AC_LOG("%s: HideCarets()", __FUNCTION__);
747 0 : HideCarets();
748 : }
749 0 : }
750 :
751 : void
752 0 : AccessibleCaretManager::OnFrameReconstruction()
753 : {
754 0 : mFirstCaret->EnsureApzAware();
755 0 : mSecondCaret->EnsureApzAware();
756 0 : }
757 :
758 : void
759 0 : AccessibleCaretManager::SetLastInputSource(uint16_t aInputSource)
760 : {
761 0 : mLastInputSource = aInputSource;
762 0 : }
763 :
764 : Selection*
765 0 : AccessibleCaretManager::GetSelection() const
766 : {
767 0 : RefPtr<nsFrameSelection> fs = GetFrameSelection();
768 0 : if (!fs) {
769 0 : return nullptr;
770 : }
771 0 : return fs->GetSelection(SelectionType::eNormal);
772 : }
773 :
774 : already_AddRefed<nsFrameSelection>
775 0 : AccessibleCaretManager::GetFrameSelection() const
776 : {
777 0 : if (!mPresShell) {
778 0 : return nullptr;
779 : }
780 :
781 0 : nsFocusManager* fm = nsFocusManager::GetFocusManager();
782 0 : MOZ_ASSERT(fm);
783 :
784 0 : nsIContent* focusedContent = fm->GetFocusedContent();
785 0 : if (focusedContent) {
786 0 : nsIFrame* focusFrame = focusedContent->GetPrimaryFrame();
787 0 : if (!focusFrame) {
788 0 : return nullptr;
789 : }
790 :
791 : // Prevent us from touching the nsFrameSelection associated with other
792 : // PresShell.
793 0 : RefPtr<nsFrameSelection> fs = focusFrame->GetFrameSelection();
794 0 : if (!fs || fs->GetShell() != mPresShell) {
795 0 : return nullptr;
796 : }
797 :
798 0 : return fs.forget();
799 : } else {
800 : // For non-editable content
801 0 : return mPresShell->FrameSelection();
802 : }
803 : }
804 :
805 : nsAutoString
806 0 : AccessibleCaretManager::StringifiedSelection() const
807 : {
808 0 : nsAutoString str;
809 0 : Selection* selection = GetSelection();
810 0 : if (selection) {
811 0 : selection->Stringify(str);
812 : }
813 0 : return str;
814 : }
815 :
816 : Element*
817 0 : AccessibleCaretManager::GetEditingHostForFrame(nsIFrame* aFrame) const
818 : {
819 0 : if (!aFrame) {
820 0 : return nullptr;
821 : }
822 :
823 0 : auto content = aFrame->GetContent();
824 0 : if (!content) {
825 0 : return nullptr;
826 : }
827 :
828 0 : return content->GetEditingHost();
829 : }
830 :
831 :
832 : AccessibleCaretManager::CaretMode
833 0 : AccessibleCaretManager::GetCaretMode() const
834 : {
835 0 : Selection* selection = GetSelection();
836 0 : if (!selection) {
837 0 : return CaretMode::None;
838 : }
839 :
840 0 : uint32_t rangeCount = selection->RangeCount();
841 0 : if (rangeCount <= 0) {
842 0 : return CaretMode::None;
843 : }
844 :
845 0 : if (selection->IsCollapsed()) {
846 0 : return CaretMode::Cursor;
847 : }
848 :
849 0 : return CaretMode::Selection;
850 : }
851 :
852 : nsIFrame*
853 0 : AccessibleCaretManager::GetFocusableFrame(nsIFrame* aFrame) const
854 : {
855 : // This implementation is similar to EventStateManager::PostHandleEvent().
856 : // Look for the nearest enclosing focusable frame.
857 0 : nsIFrame* focusableFrame = aFrame;
858 0 : while (focusableFrame) {
859 0 : if (focusableFrame->IsFocusable(nullptr, true)) {
860 0 : break;
861 : }
862 0 : focusableFrame = focusableFrame->GetParent();
863 : }
864 0 : return focusableFrame;
865 : }
866 :
867 : void
868 0 : AccessibleCaretManager::ChangeFocusToOrClearOldFocus(nsIFrame* aFrame) const
869 : {
870 0 : nsFocusManager* fm = nsFocusManager::GetFocusManager();
871 0 : MOZ_ASSERT(fm);
872 :
873 0 : if (aFrame) {
874 0 : nsIContent* focusableContent = aFrame->GetContent();
875 0 : MOZ_ASSERT(focusableContent, "Focusable frame must have content!");
876 0 : nsCOMPtr<nsIDOMElement> focusableElement = do_QueryInterface(focusableContent);
877 0 : fm->SetFocus(focusableElement, nsIFocusManager::FLAG_BYMOUSE);
878 : } else {
879 0 : nsPIDOMWindowOuter* win = mPresShell->GetDocument()->GetWindow();
880 0 : if (win) {
881 0 : fm->ClearFocus(win);
882 0 : fm->SetFocusedWindow(win);
883 : }
884 : }
885 0 : }
886 :
887 : nsresult
888 0 : AccessibleCaretManager::SelectWord(nsIFrame* aFrame, const nsPoint& aPoint) const
889 : {
890 0 : SetSelectionDragState(true);
891 0 : nsFrame* frame = static_cast<nsFrame*>(aFrame);
892 0 : nsresult rs = frame->SelectByTypeAtPoint(mPresShell->GetPresContext(), aPoint,
893 0 : eSelectWord, eSelectWord, 0);
894 :
895 0 : SetSelectionDragState(false);
896 0 : ClearMaintainedSelection();
897 :
898 : // Smart-select phone numbers if possible.
899 0 : if (sExtendSelectionForPhoneNumber) {
900 0 : SelectMoreIfPhoneNumber();
901 : }
902 :
903 0 : return rs;
904 : }
905 :
906 : void
907 0 : AccessibleCaretManager::SetSelectionDragState(bool aState) const
908 : {
909 0 : RefPtr<nsFrameSelection> fs = GetFrameSelection();
910 0 : if (fs) {
911 0 : fs->SetDragState(aState);
912 : }
913 :
914 : // Pin Fennecs DynamicToolbarAnimator in place before/after dragging,
915 : // to avoid co-incident screen scrolling.
916 : #ifdef MOZ_WIDGET_ANDROID
917 : nsIDocument* doc = mPresShell->GetDocument();
918 : MOZ_ASSERT(doc);
919 : nsIWidget* widget = nsContentUtils::WidgetForDocument(doc);
920 : static_cast<nsWindow*>(widget)->SetSelectionDragState(aState);
921 : #endif
922 0 : }
923 :
924 : bool
925 0 : AccessibleCaretManager::IsPhoneNumber(nsAString& aCandidate) const
926 : {
927 0 : RefPtr<nsIDocument> doc = mPresShell->GetDocument();
928 : nsAutoString phoneNumberRegex(
929 0 : NS_LITERAL_STRING("(^\\+)?[0-9 ,\\-.()*#pw]{1,30}$"));
930 0 : return nsContentUtils::IsPatternMatching(aCandidate, phoneNumberRegex, doc);
931 : }
932 :
933 : void
934 0 : AccessibleCaretManager::SelectMoreIfPhoneNumber() const
935 : {
936 0 : nsAutoString selectedText = StringifiedSelection();
937 :
938 0 : if (IsPhoneNumber(selectedText)) {
939 0 : SetSelectionDirection(eDirNext);
940 0 : ExtendPhoneNumberSelection(NS_LITERAL_STRING("forward"));
941 :
942 0 : SetSelectionDirection(eDirPrevious);
943 0 : ExtendPhoneNumberSelection(NS_LITERAL_STRING("backward"));
944 :
945 0 : SetSelectionDirection(eDirNext);
946 : }
947 0 : }
948 :
949 : void
950 0 : AccessibleCaretManager::ExtendPhoneNumberSelection(const nsAString& aDirection) const
951 : {
952 0 : if (!mPresShell) {
953 0 : return;
954 : }
955 :
956 : // Extend the phone number selection until we find a boundary.
957 0 : RefPtr<Selection> selection = GetSelection();
958 :
959 0 : while (selection) {
960 0 : const nsRange* anchorFocusRange = selection->GetAnchorFocusRange();
961 0 : if (!anchorFocusRange) {
962 0 : return;
963 : }
964 :
965 : // Backup the anchor focus range since both anchor node and focus node might
966 : // be changed after calling Selection::Modify().
967 0 : RefPtr<nsRange> oldAnchorFocusRange = anchorFocusRange->CloneRange();
968 :
969 : // Save current focus node, focus offset and the selected text so that
970 : // we can compare them with the modified ones later.
971 0 : nsINode* oldFocusNode = selection->GetFocusNode();
972 0 : uint32_t oldFocusOffset = selection->FocusOffset();
973 0 : nsAutoString oldSelectedText = StringifiedSelection();
974 :
975 : // Extend the selection by one char.
976 0 : selection->Modify(NS_LITERAL_STRING("extend"),
977 : aDirection,
978 0 : NS_LITERAL_STRING("character"));
979 0 : if (IsTerminated()) {
980 0 : return;
981 : }
982 :
983 : // If the selection didn't change, (can't extend further), we're done.
984 0 : if (selection->GetFocusNode() == oldFocusNode &&
985 0 : selection->FocusOffset() == oldFocusOffset) {
986 0 : return;
987 : }
988 :
989 : // If the changed selection isn't a valid phone number, we're done.
990 : // Also, if the selection was extended to a new block node, the string
991 : // returned by stringify() won't have a new line at the beginning or the
992 : // end of the string. Therefore, if either focus node or offset is
993 : // changed, but selected text is not changed, we're done, too.
994 0 : nsAutoString selectedText = StringifiedSelection();
995 :
996 0 : if (!IsPhoneNumber(selectedText) || oldSelectedText == selectedText) {
997 : // Backout the undesired selection extend, restore the old anchor focus
998 : // range before exit.
999 0 : selection->SetAnchorFocusToRange(oldAnchorFocusRange);
1000 0 : return;
1001 : }
1002 : }
1003 : }
1004 :
1005 : void
1006 0 : AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const
1007 : {
1008 0 : Selection* selection = GetSelection();
1009 0 : if (selection) {
1010 0 : selection->AdjustAnchorFocusForMultiRange(aDir);
1011 : }
1012 0 : }
1013 :
1014 : void
1015 0 : AccessibleCaretManager::ClearMaintainedSelection() const
1016 : {
1017 : // Selection made by double-clicking for example will maintain the original
1018 : // word selection. We should clear it so that we can drag caret freely.
1019 0 : RefPtr<nsFrameSelection> fs = GetFrameSelection();
1020 0 : if (fs) {
1021 0 : fs->MaintainSelection(eSelectNoAmount);
1022 : }
1023 0 : }
1024 :
1025 : void
1026 0 : AccessibleCaretManager::FlushLayout() const
1027 : {
1028 0 : if (mPresShell) {
1029 0 : mPresShell->FlushPendingNotifications(FlushType::Layout);
1030 : }
1031 0 : }
1032 :
1033 : nsIFrame*
1034 0 : AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd(
1035 : nsDirection aDirection,
1036 : int32_t* aOutOffset,
1037 : nsIContent** aOutContent,
1038 : int32_t* aOutContentOffset) const
1039 : {
1040 0 : if (!mPresShell) {
1041 0 : return nullptr;
1042 : }
1043 :
1044 0 : MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1045 0 : MOZ_ASSERT(aOutOffset, "aOutOffset shouldn't be nullptr!");
1046 :
1047 0 : nsRange* range = nullptr;
1048 0 : RefPtr<nsINode> startNode;
1049 0 : RefPtr<nsINode> endNode;
1050 0 : int32_t nodeOffset = 0;
1051 : CaretAssociationHint hint;
1052 :
1053 0 : RefPtr<Selection> selection = GetSelection();
1054 0 : bool findInFirstRangeStart = aDirection == eDirNext;
1055 :
1056 0 : if (findInFirstRangeStart) {
1057 0 : range = selection->GetRangeAt(0);
1058 0 : startNode = range->GetStartContainer();
1059 0 : endNode = range->GetEndContainer();
1060 0 : nodeOffset = range->StartOffset();
1061 0 : hint = CARET_ASSOCIATE_AFTER;
1062 : } else {
1063 0 : range = selection->GetRangeAt(selection->RangeCount() - 1);
1064 0 : startNode = range->GetEndContainer();
1065 0 : endNode = range->GetStartContainer();
1066 0 : nodeOffset = range->EndOffset();
1067 0 : hint = CARET_ASSOCIATE_BEFORE;
1068 : }
1069 :
1070 0 : nsCOMPtr<nsIContent> startContent = do_QueryInterface(startNode);
1071 0 : RefPtr<nsFrameSelection> fs = GetFrameSelection();
1072 : nsIFrame* startFrame =
1073 0 : fs->GetFrameForNodeOffset(startContent, nodeOffset, hint, aOutOffset);
1074 :
1075 0 : if (!startFrame) {
1076 0 : ErrorResult err;
1077 0 : RefPtr<TreeWalker> walker = mPresShell->GetDocument()->CreateTreeWalker(
1078 0 : *startNode, nsIDOMNodeFilter::SHOW_ALL, nullptr, err);
1079 :
1080 0 : if (!walker) {
1081 0 : return nullptr;
1082 : }
1083 :
1084 0 : startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1085 0 : while (!startFrame && startNode != endNode) {
1086 0 : startNode = findInFirstRangeStart ? walker->NextNode(err)
1087 0 : : walker->PreviousNode(err);
1088 :
1089 0 : if (!startNode) {
1090 0 : break;
1091 : }
1092 :
1093 0 : startContent = startNode->AsContent();
1094 0 : startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1095 : }
1096 :
1097 : // We are walking among the nodes in the content tree, so the node offset
1098 : // relative to startNode should be set to 0.
1099 0 : nodeOffset = 0;
1100 0 : *aOutOffset = 0;
1101 : }
1102 :
1103 0 : if (startFrame) {
1104 0 : if (aOutContent) {
1105 0 : startContent.forget(aOutContent);
1106 : }
1107 0 : if (aOutContentOffset) {
1108 0 : *aOutContentOffset = nodeOffset;
1109 : }
1110 : }
1111 :
1112 0 : return startFrame;
1113 : }
1114 :
1115 : bool
1116 0 : AccessibleCaretManager::RestrictCaretDraggingOffsets(
1117 : nsIFrame::ContentOffsets& aOffsets)
1118 : {
1119 0 : if (!mPresShell) {
1120 0 : return false;
1121 : }
1122 :
1123 0 : MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1124 :
1125 0 : nsDirection dir = mActiveCaret == mFirstCaret.get() ? eDirPrevious : eDirNext;
1126 0 : int32_t offset = 0;
1127 0 : nsCOMPtr<nsIContent> content;
1128 0 : int32_t contentOffset = 0;
1129 : nsIFrame* frame =
1130 0 : GetFrameForFirstRangeStartOrLastRangeEnd(dir, &offset,
1131 0 : getter_AddRefs(content),
1132 0 : &contentOffset);
1133 :
1134 0 : if (!frame) {
1135 0 : return false;
1136 : }
1137 :
1138 :
1139 : // Compare the active caret's new position (aOffsets) to the inactive caret's
1140 : // position.
1141 : int32_t cmpToInactiveCaretPos =
1142 0 : nsContentUtils::ComparePoints(aOffsets.content, aOffsets.StartOffset(),
1143 0 : content, contentOffset);
1144 :
1145 : // Move one character (in the direction of dir) from the inactive caret's
1146 : // position. This is the limit for the active caret's new position.
1147 0 : nsPeekOffsetStruct limit(eSelectCluster, dir, offset, nsPoint(0, 0), true, true,
1148 0 : false, false, false);
1149 0 : nsresult rv = frame->PeekOffset(&limit);
1150 0 : if (NS_FAILED(rv)) {
1151 0 : limit.mResultContent = content;
1152 0 : limit.mContentOffset = contentOffset;
1153 : }
1154 :
1155 : // Compare the active caret's new position (aOffsets) to the limit.
1156 : int32_t cmpToLimit =
1157 0 : nsContentUtils::ComparePoints(aOffsets.content, aOffsets.StartOffset(),
1158 0 : limit.mResultContent, limit.mContentOffset);
1159 :
1160 0 : auto SetOffsetsToLimit = [&aOffsets, &limit] () {
1161 0 : aOffsets.content = limit.mResultContent;
1162 0 : aOffsets.offset = limit.mContentOffset;
1163 0 : aOffsets.secondaryOffset = limit.mContentOffset;
1164 0 : };
1165 :
1166 0 : if (!sCaretsAllowDraggingAcrossOtherCaret) {
1167 0 : if ((mActiveCaret == mFirstCaret.get() && cmpToLimit == 1) ||
1168 0 : (mActiveCaret == mSecondCaret.get() && cmpToLimit == -1)) {
1169 : // The active caret's position is past the limit, which we don't allow
1170 : // here. So set it to the limit, resulting in one character being
1171 : // selected.
1172 0 : SetOffsetsToLimit();
1173 : }
1174 : } else {
1175 0 : switch (cmpToInactiveCaretPos) {
1176 : case 0:
1177 : // The active caret's position is the same as the position of the
1178 : // inactive caret. So set it to the limit to prevent the selection from
1179 : // being collapsed, resulting in one character being selected.
1180 0 : SetOffsetsToLimit();
1181 0 : break;
1182 : case 1:
1183 0 : if (mActiveCaret == mFirstCaret.get()) {
1184 : // First caret was moved across the second caret. After making change
1185 : // to the selection, the user will drag the second caret.
1186 0 : mActiveCaret = mSecondCaret.get();
1187 : }
1188 0 : break;
1189 : case -1:
1190 0 : if (mActiveCaret == mSecondCaret.get()) {
1191 : // Second caret was moved across the first caret. After making change
1192 : // to the selection, the user will drag the first caret.
1193 0 : mActiveCaret = mFirstCaret.get();
1194 : }
1195 0 : break;
1196 : }
1197 : }
1198 :
1199 0 : return true;
1200 : }
1201 :
1202 : bool
1203 0 : AccessibleCaretManager::CompareTreePosition(nsIFrame* aStartFrame,
1204 : nsIFrame* aEndFrame) const
1205 : {
1206 0 : return (aStartFrame && aEndFrame &&
1207 0 : nsLayoutUtils::CompareTreePosition(aStartFrame, aEndFrame) <= 0);
1208 : }
1209 :
1210 : nsresult
1211 0 : AccessibleCaretManager::DragCaretInternal(const nsPoint& aPoint)
1212 : {
1213 0 : if (!mPresShell) {
1214 0 : return NS_ERROR_NULL_POINTER;
1215 : }
1216 :
1217 0 : nsIFrame* rootFrame = mPresShell->GetRootFrame();
1218 0 : if (!rootFrame) {
1219 0 : return NS_ERROR_NULL_POINTER;
1220 : }
1221 :
1222 0 : nsPoint point = AdjustDragBoundary(aPoint);
1223 :
1224 : // Find out which content we point to
1225 0 : nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(
1226 : rootFrame, point,
1227 0 : nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | nsLayoutUtils::IGNORE_CROSS_DOC);
1228 0 : if (!ptFrame) {
1229 0 : return NS_ERROR_FAILURE;
1230 : }
1231 :
1232 0 : RefPtr<nsFrameSelection> fs = GetFrameSelection();
1233 0 : if (!fs) {
1234 0 : return NS_ERROR_NULL_POINTER;
1235 : }
1236 :
1237 : nsresult result;
1238 0 : nsIFrame* newFrame = nullptr;
1239 0 : nsPoint newPoint;
1240 0 : nsPoint ptInFrame = point;
1241 0 : nsLayoutUtils::TransformPoint(rootFrame, ptFrame, ptInFrame);
1242 0 : result = fs->ConstrainFrameAndPointToAnchorSubtree(ptFrame, ptInFrame,
1243 0 : &newFrame, newPoint);
1244 0 : if (NS_FAILED(result) || !newFrame) {
1245 0 : return NS_ERROR_FAILURE;
1246 : }
1247 :
1248 0 : if (!newFrame->IsSelectable(nullptr)) {
1249 0 : return NS_ERROR_FAILURE;
1250 : }
1251 :
1252 : nsIFrame::ContentOffsets offsets =
1253 0 : newFrame->GetContentOffsetsFromPoint(newPoint);
1254 0 : if (offsets.IsNull()) {
1255 0 : return NS_ERROR_FAILURE;
1256 : }
1257 :
1258 0 : Selection* selection = GetSelection();
1259 0 : if (!selection) {
1260 0 : return NS_ERROR_NULL_POINTER;
1261 : }
1262 :
1263 0 : if (GetCaretMode() == CaretMode::Selection &&
1264 0 : !RestrictCaretDraggingOffsets(offsets)) {
1265 0 : return NS_ERROR_FAILURE;
1266 : }
1267 :
1268 0 : ClearMaintainedSelection();
1269 :
1270 0 : nsIFrame* anchorFrame = nullptr;
1271 0 : selection->GetPrimaryFrameForAnchorNode(&anchorFrame);
1272 :
1273 : nsIFrame* scrollable =
1274 0 : nsLayoutUtils::GetClosestFrameOfType(anchorFrame, LayoutFrameType::Scroll);
1275 0 : AutoWeakFrame weakScrollable = scrollable;
1276 0 : fs->HandleClick(offsets.content, offsets.StartOffset(), offsets.EndOffset(),
1277 0 : GetCaretMode() == CaretMode::Selection, false,
1278 0 : offsets.associate);
1279 0 : if (!weakScrollable.IsAlive()) {
1280 0 : return NS_OK;
1281 : }
1282 :
1283 : // Scroll scrolled frame.
1284 0 : nsIScrollableFrame* saf = do_QueryFrame(scrollable);
1285 0 : nsIFrame* capturingFrame = saf->GetScrolledFrame();
1286 0 : nsPoint ptInScrolled = point;
1287 0 : nsLayoutUtils::TransformPoint(rootFrame, capturingFrame, ptInScrolled);
1288 0 : fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, kAutoScrollTimerDelay);
1289 0 : return NS_OK;
1290 : }
1291 :
1292 : nsRect
1293 0 : AccessibleCaretManager::GetAllChildFrameRectsUnion(nsIFrame* aFrame) const
1294 : {
1295 0 : nsRect unionRect;
1296 :
1297 : // Drill through scroll frames, we don't want to include scrollbar child
1298 : // frames below.
1299 0 : for (nsIFrame* frame = aFrame->GetContentInsertionFrame();
1300 0 : frame;
1301 0 : frame = frame->GetNextContinuation()) {
1302 0 : nsRect frameRect;
1303 :
1304 0 : for (nsIFrame::ChildListIterator lists(frame); !lists.IsDone(); lists.Next()) {
1305 : // Loop all children to union their scrollable overflow rect.
1306 0 : for (nsIFrame* child : lists.CurrentList()) {
1307 0 : nsRect childRect = child->GetScrollableOverflowRectRelativeToSelf();
1308 0 : nsLayoutUtils::TransformRect(child, frame, childRect);
1309 :
1310 : // A TextFrame containing only '\n' has positive height and width 0, or
1311 : // positive width and height 0 if it's vertical. Need to use UnionEdges
1312 : // to add its rect. BRFrame rect should be non-empty.
1313 0 : if (childRect.IsEmpty()) {
1314 0 : frameRect = frameRect.UnionEdges(childRect);
1315 : } else {
1316 0 : frameRect = frameRect.Union(childRect);
1317 : }
1318 : }
1319 : }
1320 :
1321 0 : MOZ_ASSERT(!frameRect.IsEmpty(),
1322 : "Editable frames should have at least one BRFrame child to make "
1323 : "frameRect non-empty!");
1324 0 : if (frame != aFrame) {
1325 0 : nsLayoutUtils::TransformRect(frame, aFrame, frameRect);
1326 : }
1327 0 : unionRect = unionRect.Union(frameRect);
1328 : }
1329 :
1330 0 : return unionRect;
1331 : }
1332 :
1333 : nsPoint
1334 0 : AccessibleCaretManager::AdjustDragBoundary(const nsPoint& aPoint) const
1335 : {
1336 0 : nsPoint adjustedPoint = aPoint;
1337 :
1338 0 : int32_t focusOffset = 0;
1339 : nsIFrame* focusFrame =
1340 0 : nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &focusOffset);
1341 0 : Element* editingHost = GetEditingHostForFrame(focusFrame);
1342 :
1343 0 : if (editingHost) {
1344 0 : nsIFrame* editingHostFrame = editingHost->GetPrimaryFrame();
1345 0 : if (editingHostFrame) {
1346 0 : nsRect boundary = GetAllChildFrameRectsUnion(editingHostFrame);
1347 0 : nsLayoutUtils::TransformRect(editingHostFrame, mPresShell->GetRootFrame(),
1348 0 : boundary);
1349 :
1350 : // Shrink the rect to make sure we never hit the boundary.
1351 0 : boundary.Deflate(kBoundaryAppUnits);
1352 :
1353 0 : adjustedPoint = boundary.ClampPoint(adjustedPoint);
1354 : }
1355 : }
1356 :
1357 0 : if (GetCaretMode() == CaretMode::Selection &&
1358 0 : !sCaretsAllowDraggingAcrossOtherCaret) {
1359 : // Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt
1360 : // mode when a caret is being dragged surpass the other caret.
1361 : //
1362 : // For example, when dragging the second caret, the horizontal boundary (lower
1363 : // bound) of its Y-coordinate is the logical position of the first caret.
1364 : // Likewise, when dragging the first caret, the horizontal boundary (upper
1365 : // bound) of its Y-coordinate is the logical position of the second caret.
1366 0 : if (mActiveCaret == mFirstCaret.get()) {
1367 0 : nscoord dragDownBoundaryY = mSecondCaret->LogicalPosition().y;
1368 0 : if (dragDownBoundaryY > 0 && adjustedPoint.y > dragDownBoundaryY) {
1369 0 : adjustedPoint.y = dragDownBoundaryY;
1370 : }
1371 : } else {
1372 0 : nscoord dragUpBoundaryY = mFirstCaret->LogicalPosition().y;
1373 0 : if (adjustedPoint.y < dragUpBoundaryY) {
1374 0 : adjustedPoint.y = dragUpBoundaryY;
1375 : }
1376 : }
1377 : }
1378 :
1379 0 : return adjustedPoint;
1380 : }
1381 :
1382 : void
1383 0 : AccessibleCaretManager::DispatchCaretStateChangedEvent(CaretChangedReason aReason) const
1384 : {
1385 0 : if (!mPresShell) {
1386 0 : return;
1387 : }
1388 :
1389 0 : FlushLayout();
1390 0 : if (IsTerminated()) {
1391 0 : return;
1392 : }
1393 :
1394 0 : Selection* sel = GetSelection();
1395 0 : if (!sel) {
1396 0 : return;
1397 : }
1398 :
1399 0 : nsIDocument* doc = mPresShell->GetDocument();
1400 0 : MOZ_ASSERT(doc);
1401 :
1402 0 : CaretStateChangedEventInit init;
1403 0 : init.mBubbles = true;
1404 :
1405 0 : const nsRange* range = sel->GetAnchorFocusRange();
1406 0 : nsINode* commonAncestorNode = nullptr;
1407 0 : if (range) {
1408 0 : commonAncestorNode = range->GetCommonAncestor();
1409 : }
1410 :
1411 0 : if (!commonAncestorNode) {
1412 0 : commonAncestorNode = sel->GetFrameSelection()->GetAncestorLimiter();
1413 : }
1414 :
1415 0 : RefPtr<DOMRect> domRect = new DOMRect(ToSupports(doc));
1416 0 : nsRect rect = nsLayoutUtils::GetSelectionBoundingRect(sel);
1417 :
1418 0 : nsIFrame* commonAncestorFrame = nullptr;
1419 0 : nsIFrame* rootFrame = mPresShell->GetRootFrame();
1420 :
1421 0 : if (commonAncestorNode && commonAncestorNode->IsContent()) {
1422 0 : commonAncestorFrame = commonAncestorNode->AsContent()->GetPrimaryFrame();
1423 : }
1424 :
1425 0 : if (commonAncestorFrame && rootFrame) {
1426 0 : nsLayoutUtils::TransformRect(rootFrame, commonAncestorFrame, rect);
1427 : nsRect clampedRect = nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame,
1428 0 : rect);
1429 0 : nsLayoutUtils::TransformRect(commonAncestorFrame, rootFrame, clampedRect);
1430 0 : domRect->SetLayoutRect(clampedRect);
1431 0 : init.mSelectionVisible = !clampedRect.IsEmpty();
1432 : } else {
1433 0 : domRect->SetLayoutRect(rect);
1434 0 : init.mSelectionVisible = true;
1435 : }
1436 :
1437 : // Send isEditable info w/ event detail. This info can help determine
1438 : // whether to show cut command on selection dialog or not.
1439 0 : init.mSelectionEditable = commonAncestorFrame &&
1440 0 : GetEditingHostForFrame(commonAncestorFrame);
1441 :
1442 0 : init.mBoundingClientRect = domRect;
1443 0 : init.mReason = aReason;
1444 0 : init.mCollapsed = sel->IsCollapsed();
1445 0 : init.mCaretVisible = mFirstCaret->IsLogicallyVisible() ||
1446 0 : mSecondCaret->IsLogicallyVisible();
1447 0 : init.mCaretVisuallyVisible = mFirstCaret->IsVisuallyVisible() ||
1448 0 : mSecondCaret->IsVisuallyVisible();
1449 0 : sel->Stringify(init.mSelectedTextContent);
1450 :
1451 : RefPtr<CaretStateChangedEvent> event =
1452 0 : CaretStateChangedEvent::Constructor(doc, NS_LITERAL_STRING("mozcaretstatechanged"), init);
1453 :
1454 0 : event->SetTrusted(true);
1455 0 : event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
1456 :
1457 0 : AC_LOG("%s: reason %" PRIu32 ", collapsed %d, caretVisible %" PRIu32, __FUNCTION__,
1458 : static_cast<uint32_t>(init.mReason), init.mCollapsed,
1459 : static_cast<uint32_t>(init.mCaretVisible));
1460 :
1461 0 : (new AsyncEventDispatcher(doc, event))->RunDOMEventWhenSafe();
1462 : }
1463 :
1464 : } // namespace mozilla
|