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