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 /* the caret is the text cursor used, e.g., when editing */
8
9 #include "nsCaret.h"
10
11 #include <algorithm>
12
13 #include "gfxUtils.h"
14 #include "mozilla/gfx/2D.h"
15 #include "nsCOMPtr.h"
16 #include "nsFontMetrics.h"
17 #include "nsITimer.h"
18 #include "nsFrameSelection.h"
19 #include "nsIFrame.h"
20 #include "nsIScrollableFrame.h"
21 #include "nsIDOMNode.h"
22 #include "nsISelection.h"
23 #include "nsISelectionPrivate.h"
24 #include "nsIContent.h"
25 #include "nsIPresShell.h"
26 #include "nsLayoutUtils.h"
27 #include "nsPresContext.h"
28 #include "nsBlockFrame.h"
29 #include "nsISelectionController.h"
30 #include "nsTextFrame.h"
31 #include "nsXULPopupManager.h"
32 #include "nsMenuPopupFrame.h"
33 #include "nsTextFragment.h"
34 #include "mozilla/Preferences.h"
35 #include "mozilla/LookAndFeel.h"
36 #include "nsIBidiKeyboard.h"
37 #include "nsContentUtils.h"
38
39 using namespace mozilla;
40 using namespace mozilla::dom;
41 using namespace mozilla::gfx;
42
43 // The bidi indicator hangs off the caret to one side, to show which
44 // direction the typing is in. It needs to be at least 2x2 to avoid looking like
45 // an insignificant dot
46 static const int32_t kMinBidiIndicatorPixels = 2;
47
48 // The default caret blinking rate (in ms of blinking interval)
49 static const uint32_t kDefaultCaretBlinkRate = 500;
50
51 /**
52 * Find the first frame in an in-order traversal of the frame subtree rooted
53 * at aFrame which is either a text frame logically at the end of a line,
54 * or which is aStopAtFrame. Return null if no such frame is found. We don't
55 * descend into the children of non-eLineParticipant frames.
56 */
CheckForTrailingTextFrameRecursive(nsIFrame * aFrame,nsIFrame * aStopAtFrame)57 static nsIFrame* CheckForTrailingTextFrameRecursive(nsIFrame* aFrame,
58 nsIFrame* aStopAtFrame) {
59 if (aFrame == aStopAtFrame ||
60 ((aFrame->IsTextFrame() &&
61 (static_cast<nsTextFrame*>(aFrame))->IsAtEndOfLine())))
62 return aFrame;
63 if (!aFrame->IsFrameOfType(nsIFrame::eLineParticipant)) return nullptr;
64
65 for (nsIFrame* f : aFrame->PrincipalChildList()) {
66 nsIFrame* r = CheckForTrailingTextFrameRecursive(f, aStopAtFrame);
67 if (r) return r;
68 }
69 return nullptr;
70 }
71
FindContainingLine(nsIFrame * aFrame)72 static nsLineBox* FindContainingLine(nsIFrame* aFrame) {
73 while (aFrame && aFrame->IsFrameOfType(nsIFrame::eLineParticipant)) {
74 nsIFrame* parent = aFrame->GetParent();
75 nsBlockFrame* blockParent = nsLayoutUtils::GetAsBlock(parent);
76 if (blockParent) {
77 bool isValid;
78 nsBlockInFlowLineIterator iter(blockParent, aFrame, &isValid);
79 return isValid ? iter.GetLine().get() : nullptr;
80 }
81 aFrame = parent;
82 }
83 return nullptr;
84 }
85
AdjustCaretFrameForLineEnd(nsIFrame ** aFrame,int32_t * aOffset)86 static void AdjustCaretFrameForLineEnd(nsIFrame** aFrame, int32_t* aOffset) {
87 nsLineBox* line = FindContainingLine(*aFrame);
88 if (!line) return;
89 int32_t count = line->GetChildCount();
90 for (nsIFrame *f = line->mFirstChild; count > 0;
91 --count, f = f->GetNextSibling()) {
92 nsIFrame* r = CheckForTrailingTextFrameRecursive(f, *aFrame);
93 if (r == *aFrame) return;
94 if (r) {
95 *aFrame = r;
96 NS_ASSERTION(r->IsTextFrame(), "Expected text frame");
97 *aOffset = (static_cast<nsTextFrame*>(r))->GetContentEnd();
98 return;
99 }
100 }
101 }
102
IsBidiUI()103 static bool IsBidiUI() { return Preferences::GetBool("bidi.browser.ui"); }
104
nsCaret()105 nsCaret::nsCaret()
106 : mOverrideOffset(0),
107 mBlinkCount(-1),
108 mBlinkRate(0),
109 mHideCount(0),
110 mIsBlinkOn(false),
111 mVisible(false),
112 mReadOnly(false),
113 mShowDuringSelection(false),
114 mIgnoreUserModify(true) {}
115
~nsCaret()116 nsCaret::~nsCaret() { StopBlinking(); }
117
Init(nsIPresShell * inPresShell)118 nsresult nsCaret::Init(nsIPresShell* inPresShell) {
119 NS_ENSURE_ARG(inPresShell);
120
121 mPresShell =
122 do_GetWeakReference(inPresShell); // the presshell owns us, so no addref
123 NS_ASSERTION(mPresShell, "Hey, pres shell should support weak refs");
124
125 mShowDuringSelection =
126 LookAndFeel::GetInt(LookAndFeel::eIntID_ShowCaretDuringSelection,
127 mShowDuringSelection ? 1 : 0) != 0;
128
129 // get the selection from the pres shell, and set ourselves up as a selection
130 // listener
131
132 nsCOMPtr<nsISelectionController> selCon = do_QueryReferent(mPresShell);
133 if (!selCon) return NS_ERROR_FAILURE;
134
135 nsCOMPtr<nsISelection> domSelection;
136 nsresult rv = selCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
137 getter_AddRefs(domSelection));
138 if (NS_FAILED(rv)) return rv;
139 if (!domSelection) return NS_ERROR_FAILURE;
140
141 nsCOMPtr<nsISelectionPrivate> privateSelection =
142 do_QueryInterface(domSelection);
143 if (privateSelection) privateSelection->AddSelectionListener(this);
144 mDomSelectionWeak = do_GetWeakReference(domSelection);
145
146 return NS_OK;
147 }
148
DrawCJKCaret(nsIFrame * aFrame,int32_t aOffset)149 static bool DrawCJKCaret(nsIFrame* aFrame, int32_t aOffset) {
150 nsIContent* content = aFrame->GetContent();
151 const nsTextFragment* frag = content->GetText();
152 if (!frag) return false;
153 if (aOffset < 0 || uint32_t(aOffset) >= frag->GetLength()) return false;
154 char16_t ch = frag->CharAt(aOffset);
155 return 0x2e80 <= ch && ch <= 0xd7ff;
156 }
157
ComputeMetrics(nsIFrame * aFrame,int32_t aOffset,nscoord aCaretHeight)158 nsCaret::Metrics nsCaret::ComputeMetrics(nsIFrame* aFrame, int32_t aOffset,
159 nscoord aCaretHeight) {
160 // Compute nominal sizes in appunits
161 nscoord caretWidth =
162 (aCaretHeight *
163 LookAndFeel::GetFloat(LookAndFeel::eFloatID_CaretAspectRatio, 0.0f)) +
164 nsPresContext::CSSPixelsToAppUnits(
165 LookAndFeel::GetInt(LookAndFeel::eIntID_CaretWidth, 1));
166
167 if (DrawCJKCaret(aFrame, aOffset)) {
168 caretWidth += nsPresContext::CSSPixelsToAppUnits(1);
169 }
170 nscoord bidiIndicatorSize =
171 nsPresContext::CSSPixelsToAppUnits(kMinBidiIndicatorPixels);
172 bidiIndicatorSize = std::max(caretWidth, bidiIndicatorSize);
173
174 // Round them to device pixels. Always round down, except that anything
175 // between 0 and 1 goes up to 1 so we don't let the caret disappear.
176 int32_t tpp = aFrame->PresContext()->AppUnitsPerDevPixel();
177 Metrics result;
178 result.mCaretWidth = NS_ROUND_BORDER_TO_PIXELS(caretWidth, tpp);
179 result.mBidiIndicatorSize = NS_ROUND_BORDER_TO_PIXELS(bidiIndicatorSize, tpp);
180 return result;
181 }
182
Terminate()183 void nsCaret::Terminate() {
184 // this doesn't erase the caret if it's drawn. Should it? We might not have
185 // a good drawing environment during teardown.
186
187 StopBlinking();
188 mBlinkTimer = nullptr;
189
190 // unregiser ourselves as a selection listener
191 nsCOMPtr<nsISelection> domSelection = do_QueryReferent(mDomSelectionWeak);
192 nsCOMPtr<nsISelectionPrivate> privateSelection(
193 do_QueryInterface(domSelection));
194 if (privateSelection) privateSelection->RemoveSelectionListener(this);
195 mDomSelectionWeak = nullptr;
196 mPresShell = nullptr;
197
198 mOverrideContent = nullptr;
199 }
200
NS_IMPL_ISUPPORTS(nsCaret,nsISelectionListener)201 NS_IMPL_ISUPPORTS(nsCaret, nsISelectionListener)
202
203 nsISelection* nsCaret::GetSelection() {
204 nsCOMPtr<nsISelection> sel(do_QueryReferent(mDomSelectionWeak));
205 return sel;
206 }
207
SetSelection(nsISelection * aDOMSel)208 void nsCaret::SetSelection(nsISelection* aDOMSel) {
209 MOZ_ASSERT(aDOMSel);
210 mDomSelectionWeak =
211 do_GetWeakReference(aDOMSel); // weak reference to pres shell
212 ResetBlinking();
213 SchedulePaint(aDOMSel);
214 }
215
SetVisible(bool inMakeVisible)216 void nsCaret::SetVisible(bool inMakeVisible) {
217 mVisible = inMakeVisible;
218 mIgnoreUserModify = mVisible;
219 ResetBlinking();
220 SchedulePaint();
221 }
222
AddForceHide()223 void nsCaret::AddForceHide() {
224 MOZ_ASSERT(mHideCount < UINT32_MAX);
225 if (++mHideCount > 1) {
226 return;
227 }
228 ResetBlinking();
229 SchedulePaint();
230 }
231
RemoveForceHide()232 void nsCaret::RemoveForceHide() {
233 if (!mHideCount || --mHideCount) {
234 return;
235 }
236 ResetBlinking();
237 SchedulePaint();
238 }
239
SetCaretReadOnly(bool inMakeReadonly)240 void nsCaret::SetCaretReadOnly(bool inMakeReadonly) {
241 mReadOnly = inMakeReadonly;
242 ResetBlinking();
243 SchedulePaint();
244 }
245
GetGeometryForFrame(nsIFrame * aFrame,int32_t aFrameOffset,nscoord * aBidiIndicatorSize)246 /* static */ nsRect nsCaret::GetGeometryForFrame(nsIFrame* aFrame,
247 int32_t aFrameOffset,
248 nscoord* aBidiIndicatorSize) {
249 nsPoint framePos(0, 0);
250 nsRect rect;
251 nsresult rv = aFrame->GetPointFromOffset(aFrameOffset, &framePos);
252 if (NS_FAILED(rv)) {
253 if (aBidiIndicatorSize) {
254 *aBidiIndicatorSize = 0;
255 }
256 return rect;
257 }
258
259 nsIFrame* frame = aFrame->GetContentInsertionFrame();
260 if (!frame) {
261 frame = aFrame;
262 }
263 NS_ASSERTION(!(frame->GetStateBits() & NS_FRAME_IN_REFLOW),
264 "We should not be in the middle of reflow");
265 nscoord baseline = frame->GetCaretBaseline();
266 nscoord ascent = 0, descent = 0;
267 RefPtr<nsFontMetrics> fm =
268 nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame);
269 NS_ASSERTION(fm, "We should be able to get the font metrics");
270 if (fm) {
271 ascent = fm->MaxAscent();
272 descent = fm->MaxDescent();
273 }
274 nscoord height = ascent + descent;
275 WritingMode wm = aFrame->GetWritingMode();
276 bool vertical = wm.IsVertical();
277 if (vertical) {
278 if (wm.IsLineInverted()) {
279 framePos.x = baseline - descent;
280 } else {
281 framePos.x = baseline - ascent;
282 }
283 } else {
284 framePos.y = baseline - ascent;
285 }
286 Metrics caretMetrics = ComputeMetrics(aFrame, aFrameOffset, height);
287
288 nsTextFrame* textFrame = do_QueryFrame(aFrame);
289 if (textFrame) {
290 gfxTextRun* textRun =
291 textFrame->GetTextRun(nsTextFrame::TextRunType::eInflated);
292 if (textRun) {
293 // For "upstream" text where the textrun direction is reversed from the
294 // frame's inline-dir we want the caret to be painted before rather than
295 // after its nominal inline position, so we offset by its width.
296 bool textRunDirIsReverseOfFrame =
297 wm.IsInlineReversed() != textRun->IsInlineReversed();
298 // However, in sideways-lr mode we invert this behavior because this is
299 // the one writing mode where bidi-LTR corresponds to inline-reversed
300 // already, which reverses the desired caret placement behavior.
301 // Note that the following condition is equivalent to:
302 // if ( (!textRun->IsSidewaysLeft() && textRunDirIsReverseOfFrame) ||
303 // (textRun->IsSidewaysLeft() && !textRunDirIsReverseOfFrame) )
304 if (textRunDirIsReverseOfFrame != textRun->IsSidewaysLeft()) {
305 int dir = wm.IsBidiLTR() ? -1 : 1;
306 if (vertical) {
307 framePos.y += dir * caretMetrics.mCaretWidth;
308 } else {
309 framePos.x += dir * caretMetrics.mCaretWidth;
310 }
311 }
312 }
313 }
314
315 rect = nsRect(framePos, vertical ? nsSize(height, caretMetrics.mCaretWidth)
316 : nsSize(caretMetrics.mCaretWidth, height));
317
318 // Clamp the inline-position to be within our scroll frame. If we don't, then
319 // it clips us, and we don't appear at all. See bug 335560.
320 nsIFrame* scrollFrame =
321 nsLayoutUtils::GetClosestFrameOfType(aFrame, LayoutFrameType::Scroll);
322 if (scrollFrame) {
323 // First, use the scrollFrame to get at the scrollable view that we're in.
324 nsIScrollableFrame* sf = do_QueryFrame(scrollFrame);
325 nsIFrame* scrolled = sf->GetScrolledFrame();
326 nsRect caretInScroll = rect + aFrame->GetOffsetTo(scrolled);
327
328 // Now see if the caret extends beyond the view's bounds. If it does,
329 // then snap it back, put it as close to the edge as it can.
330 if (vertical) {
331 nscoord overflow = caretInScroll.YMost() -
332 scrolled->GetVisualOverflowRectRelativeToSelf().height;
333 if (overflow > 0) {
334 rect.y -= overflow;
335 }
336 } else {
337 nscoord overflow = caretInScroll.XMost() -
338 scrolled->GetVisualOverflowRectRelativeToSelf().width;
339 if (overflow > 0) {
340 rect.x -= overflow;
341 }
342 }
343 }
344
345 if (aBidiIndicatorSize) {
346 *aBidiIndicatorSize = caretMetrics.mBidiIndicatorSize;
347 }
348 return rect;
349 }
350
GetFrameAndOffset(Selection * aSelection,nsINode * aOverrideNode,int32_t aOverrideOffset,int32_t * aFrameOffset)351 nsIFrame* nsCaret::GetFrameAndOffset(Selection* aSelection,
352 nsINode* aOverrideNode,
353 int32_t aOverrideOffset,
354 int32_t* aFrameOffset) {
355 nsINode* focusNode;
356 int32_t focusOffset;
357
358 if (aOverrideNode) {
359 focusNode = aOverrideNode;
360 focusOffset = aOverrideOffset;
361 } else if (aSelection) {
362 focusNode = aSelection->GetFocusNode();
363 aSelection->GetFocusOffset(&focusOffset);
364 } else {
365 return nullptr;
366 }
367
368 if (!focusNode || !focusNode->IsContent()) {
369 return nullptr;
370 }
371
372 nsIContent* contentNode = focusNode->AsContent();
373 nsFrameSelection* frameSelection = aSelection->GetFrameSelection();
374 nsBidiLevel bidiLevel = frameSelection->GetCaretBidiLevel();
375 nsIFrame* frame;
376 nsresult rv = nsCaret::GetCaretFrameForNodeOffset(
377 frameSelection, contentNode, focusOffset, frameSelection->GetHint(),
378 bidiLevel, &frame, aFrameOffset);
379 if (NS_FAILED(rv) || !frame) {
380 return nullptr;
381 }
382
383 return frame;
384 }
385
GetGeometry(nsISelection * aSelection,nsRect * aRect)386 /* static */ nsIFrame* nsCaret::GetGeometry(nsISelection* aSelection,
387 nsRect* aRect) {
388 int32_t frameOffset;
389 Selection* selection = aSelection ? aSelection->AsSelection() : nullptr;
390 nsIFrame* frame = GetFrameAndOffset(selection, nullptr, 0, &frameOffset);
391 if (frame) {
392 *aRect = GetGeometryForFrame(frame, frameOffset, nullptr);
393 }
394 return frame;
395 }
396
GetSelectionInternal()397 Selection* nsCaret::GetSelectionInternal() {
398 nsISelection* domSelection = GetSelection();
399 return domSelection ? domSelection->AsSelection() : nullptr;
400 }
401
SchedulePaint(nsISelection * aSelection)402 void nsCaret::SchedulePaint(nsISelection* aSelection) {
403 Selection* selection;
404 if (aSelection) {
405 selection = aSelection->AsSelection();
406 } else {
407 selection = GetSelectionInternal();
408 }
409 nsINode* focusNode;
410 if (mOverrideContent) {
411 focusNode = mOverrideContent;
412 } else if (selection) {
413 focusNode = selection->GetFocusNode();
414 } else {
415 return;
416 }
417 if (!focusNode || !focusNode->IsContent()) {
418 return;
419 }
420 nsIFrame* f = focusNode->AsContent()->GetPrimaryFrame();
421 if (!f) {
422 return;
423 }
424 // This may not be the correct continuation frame, but that's OK since we're
425 // just scheduling a paint of the window (or popup).
426 f->SchedulePaint();
427 }
428
SetVisibilityDuringSelection(bool aVisibility)429 void nsCaret::SetVisibilityDuringSelection(bool aVisibility) {
430 mShowDuringSelection = aVisibility;
431 SchedulePaint();
432 }
433
SetCaretPosition(nsIDOMNode * aNode,int32_t aOffset)434 void nsCaret::SetCaretPosition(nsIDOMNode* aNode, int32_t aOffset) {
435 mOverrideContent = do_QueryInterface(aNode);
436 mOverrideOffset = aOffset;
437
438 ResetBlinking();
439 SchedulePaint();
440 }
441
CheckSelectionLanguageChange()442 void nsCaret::CheckSelectionLanguageChange() {
443 if (!IsBidiUI()) {
444 return;
445 }
446
447 bool isKeyboardRTL = false;
448 nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard();
449 if (bidiKeyboard) {
450 bidiKeyboard->IsLangRTL(&isKeyboardRTL);
451 }
452 // Call SelectionLanguageChange on every paint. Mostly it will be a noop
453 // but it should be fast anyway. This guarantees we never paint the caret
454 // at the wrong place.
455 Selection* selection = GetSelectionInternal();
456 if (selection) {
457 selection->SelectionLanguageChange(isKeyboardRTL);
458 }
459 }
460
GetPaintGeometry(nsRect * aRect)461 nsIFrame* nsCaret::GetPaintGeometry(nsRect* aRect) {
462 // Return null if we should not be visible.
463 if (!IsVisible() || !mIsBlinkOn) {
464 return nullptr;
465 }
466
467 // Update selection language direction now so the new direction will be
468 // taken into account when computing the caret position below.
469 CheckSelectionLanguageChange();
470
471 int32_t frameOffset;
472 nsIFrame* frame = GetFrameAndOffset(GetSelectionInternal(), mOverrideContent,
473 mOverrideOffset, &frameOffset);
474 if (!frame) {
475 return nullptr;
476 }
477
478 // now we have a frame, check whether it's appropriate to show the caret here
479 const nsStyleUserInterface* userinterface = frame->StyleUserInterface();
480 if ((!mIgnoreUserModify &&
481 userinterface->mUserModify == StyleUserModify::ReadOnly) ||
482 frame->IsContentDisabled()) {
483 return nullptr;
484 }
485
486 // If the offset falls outside of the frame, then don't paint the caret.
487 int32_t startOffset, endOffset;
488 if (frame->IsTextFrame() &&
489 (NS_FAILED(frame->GetOffsets(startOffset, endOffset)) ||
490 startOffset > frameOffset || endOffset < frameOffset)) {
491 return nullptr;
492 }
493
494 nsRect caretRect;
495 nsRect hookRect;
496 ComputeCaretRects(frame, frameOffset, &caretRect, &hookRect);
497
498 aRect->UnionRect(caretRect, hookRect);
499 return frame;
500 }
501
GetFrame(int32_t * aContentOffset)502 nsIFrame* nsCaret::GetFrame(int32_t* aContentOffset) {
503 return GetFrameAndOffset(GetSelectionInternal(), mOverrideContent,
504 mOverrideOffset, aContentOffset);
505 }
506
PaintCaret(DrawTarget & aDrawTarget,nsIFrame * aForFrame,const nsPoint & aOffset)507 void nsCaret::PaintCaret(DrawTarget& aDrawTarget, nsIFrame* aForFrame,
508 const nsPoint& aOffset) {
509 int32_t contentOffset;
510 nsIFrame* frame = GetFrame(&contentOffset);
511 if (!frame) {
512 return;
513 }
514 NS_ASSERTION(frame == aForFrame, "We're referring different frame");
515
516 int32_t appUnitsPerDevPixel = frame->PresContext()->AppUnitsPerDevPixel();
517
518 nsRect caretRect;
519 nsRect hookRect;
520 ComputeCaretRects(frame, contentOffset, &caretRect, &hookRect);
521
522 Rect devPxCaretRect = NSRectToSnappedRect(caretRect + aOffset,
523 appUnitsPerDevPixel, aDrawTarget);
524 Rect devPxHookRect =
525 NSRectToSnappedRect(hookRect + aOffset, appUnitsPerDevPixel, aDrawTarget);
526 ColorPattern color(ToDeviceColor(frame->GetCaretColorAt(contentOffset)));
527
528 aDrawTarget.FillRect(devPxCaretRect, color);
529 if (!hookRect.IsEmpty()) {
530 aDrawTarget.FillRect(devPxHookRect, color);
531 }
532 }
533
534 NS_IMETHODIMP
NotifySelectionChanged(nsIDOMDocument *,nsISelection * aDomSel,int16_t aReason)535 nsCaret::NotifySelectionChanged(nsIDOMDocument*, nsISelection* aDomSel,
536 int16_t aReason) {
537 // Note that aDomSel, per the comment below may not be the same as our
538 // selection, but that's OK since if that is the case, it wouldn't have
539 // mattered what IsVisible() returns here, so we just opt for checking
540 // the selection later down below.
541 if ((aReason & nsISelectionListener::MOUSEUP_REASON) ||
542 !IsVisible(aDomSel)) // this wont do
543 return NS_OK;
544
545 nsCOMPtr<nsISelection> domSel(do_QueryReferent(mDomSelectionWeak));
546
547 // The same caret is shared amongst the document and any text widgets it
548 // may contain. This means that the caret could get notifications from
549 // multiple selections.
550 //
551 // If this notification is for a selection that is not the one the
552 // the caret is currently interested in (mDomSelectionWeak), then there
553 // is nothing to do!
554
555 if (domSel != aDomSel) return NS_OK;
556
557 ResetBlinking();
558 SchedulePaint(aDomSel);
559
560 return NS_OK;
561 }
562
ResetBlinking()563 void nsCaret::ResetBlinking() {
564 mIsBlinkOn = true;
565
566 if (mReadOnly || !mVisible || mHideCount) {
567 StopBlinking();
568 return;
569 }
570
571 uint32_t blinkRate = static_cast<uint32_t>(LookAndFeel::GetInt(
572 LookAndFeel::eIntID_CaretBlinkTime, kDefaultCaretBlinkRate));
573 if (mBlinkRate == blinkRate) {
574 // If the rate hasn't changed, then there is nothing to do.
575 return;
576 }
577 mBlinkRate = blinkRate;
578
579 if (mBlinkTimer) {
580 mBlinkTimer->Cancel();
581 } else {
582 nsIEventTarget* target = nullptr;
583 if (nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell)) {
584 if (nsCOMPtr<nsIDocument> doc = presShell->GetDocument()) {
585 target = doc->EventTargetFor(TaskCategory::Other);
586 }
587 }
588
589 mBlinkTimer = NS_NewTimer(target);
590 if (!mBlinkTimer) {
591 return;
592 }
593 }
594
595 if (blinkRate > 0) {
596 mBlinkCount = Preferences::GetInt("ui.caretBlinkCount", -1);
597 mBlinkTimer->InitWithNamedFuncCallback(CaretBlinkCallback, this, blinkRate,
598 nsITimer::TYPE_REPEATING_SLACK,
599 "nsCaret::CaretBlinkCallback_timer");
600 }
601 }
602
StopBlinking()603 void nsCaret::StopBlinking() {
604 if (mBlinkTimer) {
605 mBlinkTimer->Cancel();
606 mBlinkRate = 0;
607 }
608 }
609
GetCaretFrameForNodeOffset(nsFrameSelection * aFrameSelection,nsIContent * aContentNode,int32_t aOffset,CaretAssociationHint aFrameHint,nsBidiLevel aBidiLevel,nsIFrame ** aReturnFrame,int32_t * aReturnOffset)610 nsresult nsCaret::GetCaretFrameForNodeOffset(
611 nsFrameSelection* aFrameSelection, nsIContent* aContentNode,
612 int32_t aOffset, CaretAssociationHint aFrameHint, nsBidiLevel aBidiLevel,
613 nsIFrame** aReturnFrame, int32_t* aReturnOffset) {
614 if (!aFrameSelection) return NS_ERROR_FAILURE;
615 nsIPresShell* presShell = aFrameSelection->GetShell();
616 if (!presShell) return NS_ERROR_FAILURE;
617
618 if (!aContentNode || !aContentNode->IsInComposedDoc() ||
619 presShell->GetDocument() != aContentNode->GetComposedDoc())
620 return NS_ERROR_FAILURE;
621
622 nsIFrame* theFrame = nullptr;
623 int32_t theFrameOffset = 0;
624
625 theFrame = aFrameSelection->GetFrameForNodeOffset(
626 aContentNode, aOffset, aFrameHint, &theFrameOffset);
627 if (!theFrame) return NS_ERROR_FAILURE;
628
629 // if theFrame is after a text frame that's logically at the end of the line
630 // (e.g. if theFrame is a <br> frame), then put the caret at the end of
631 // that text frame instead. This way, the caret will be positioned as if
632 // trailing whitespace was not trimmed.
633 AdjustCaretFrameForLineEnd(&theFrame, &theFrameOffset);
634
635 // Mamdouh : modification of the caret to work at rtl and ltr with Bidi
636 //
637 // Direction Style from visibility->mDirection
638 // ------------------
639 // NS_STYLE_DIRECTION_LTR : LTR or Default
640 // NS_STYLE_DIRECTION_RTL
641 if (theFrame->PresContext()->BidiEnabled()) {
642 // If there has been a reflow, take the caret Bidi level to be the level of
643 // the current frame
644 if (aBidiLevel & BIDI_LEVEL_UNDEFINED) {
645 aBidiLevel = theFrame->GetEmbeddingLevel();
646 }
647
648 int32_t start;
649 int32_t end;
650 nsIFrame* frameBefore;
651 nsIFrame* frameAfter;
652 nsBidiLevel levelBefore; // Bidi level of the character before the caret
653 nsBidiLevel levelAfter; // Bidi level of the character after the caret
654
655 theFrame->GetOffsets(start, end);
656 if (start == 0 || end == 0 || start == theFrameOffset ||
657 end == theFrameOffset) {
658 nsPrevNextBidiLevels levels =
659 aFrameSelection->GetPrevNextBidiLevels(aContentNode, aOffset, false);
660
661 /* Boundary condition, we need to know the Bidi levels of the characters
662 * before and after the caret */
663 if (levels.mFrameBefore || levels.mFrameAfter) {
664 frameBefore = levels.mFrameBefore;
665 frameAfter = levels.mFrameAfter;
666 levelBefore = levels.mLevelBefore;
667 levelAfter = levels.mLevelAfter;
668
669 if ((levelBefore != levelAfter) || (aBidiLevel != levelBefore)) {
670 aBidiLevel = std::max(aBidiLevel,
671 std::min(levelBefore, levelAfter)); // rule c3
672 aBidiLevel = std::min(aBidiLevel,
673 std::max(levelBefore, levelAfter)); // rule c4
674 if (aBidiLevel == levelBefore // rule c1
675 || (aBidiLevel > levelBefore && aBidiLevel < levelAfter &&
676 IS_SAME_DIRECTION(aBidiLevel, levelBefore)) // rule c5
677 || (aBidiLevel < levelBefore && aBidiLevel > levelAfter &&
678 IS_SAME_DIRECTION(aBidiLevel, levelBefore))) // rule c9
679 {
680 if (theFrame != frameBefore) {
681 if (frameBefore) // if there is a frameBefore, move into it
682 {
683 theFrame = frameBefore;
684 theFrame->GetOffsets(start, end);
685 theFrameOffset = end;
686 } else {
687 // if there is no frameBefore, we must be at the beginning of
688 // the line so we stay with the current frame. Exception: when
689 // the first frame on the line has a different Bidi level from
690 // the paragraph level, there is no real frame for the caret to
691 // be in. We have to find the visually first frame on the line.
692 nsBidiLevel baseLevel = frameAfter->GetBaseLevel();
693 if (baseLevel != levelAfter) {
694 nsPeekOffsetStruct pos(eSelectBeginLine, eDirPrevious, 0,
695 nsPoint(0, 0), false, true, false,
696 true, false);
697 if (NS_SUCCEEDED(frameAfter->PeekOffset(&pos))) {
698 theFrame = pos.mResultFrame;
699 theFrameOffset = pos.mContentOffset;
700 }
701 }
702 }
703 }
704 } else if (aBidiLevel == levelAfter // rule c2
705 || (aBidiLevel > levelBefore && aBidiLevel < levelAfter &&
706 IS_SAME_DIRECTION(aBidiLevel, levelAfter)) // rule c6
707 ||
708 (aBidiLevel < levelBefore && aBidiLevel > levelAfter &&
709 IS_SAME_DIRECTION(aBidiLevel, levelAfter))) // rule c10
710 {
711 if (theFrame != frameAfter) {
712 if (frameAfter) {
713 // if there is a frameAfter, move into it
714 theFrame = frameAfter;
715 theFrame->GetOffsets(start, end);
716 theFrameOffset = start;
717 } else {
718 // if there is no frameAfter, we must be at the end of the line
719 // so we stay with the current frame.
720 // Exception: when the last frame on the line has a different
721 // Bidi level from the paragraph level, there is no real frame
722 // for the caret to be in. We have to find the visually last
723 // frame on the line.
724 nsBidiLevel baseLevel = frameBefore->GetBaseLevel();
725 if (baseLevel != levelBefore) {
726 nsPeekOffsetStruct pos(eSelectEndLine, eDirNext, 0,
727 nsPoint(0, 0), false, true, false,
728 true, false);
729 if (NS_SUCCEEDED(frameBefore->PeekOffset(&pos))) {
730 theFrame = pos.mResultFrame;
731 theFrameOffset = pos.mContentOffset;
732 }
733 }
734 }
735 }
736 } else if (aBidiLevel > levelBefore &&
737 aBidiLevel < levelAfter // rule c7/8
738 &&
739 IS_SAME_DIRECTION(
740 levelBefore,
741 levelAfter) // before and after have the same parity
742 &&
743 !IS_SAME_DIRECTION(
744 aBidiLevel, levelAfter)) // caret has different parity
745 {
746 if (NS_SUCCEEDED(aFrameSelection->GetFrameFromLevel(
747 frameAfter, eDirNext, aBidiLevel, &theFrame))) {
748 theFrame->GetOffsets(start, end);
749 levelAfter = theFrame->GetEmbeddingLevel();
750 if (IS_LEVEL_RTL(aBidiLevel)) // c8: caret to the right of the
751 // rightmost character
752 theFrameOffset = IS_LEVEL_RTL(levelAfter) ? start : end;
753 else // c7: caret to the left of the leftmost character
754 theFrameOffset = IS_LEVEL_RTL(levelAfter) ? end : start;
755 }
756 } else if (aBidiLevel < levelBefore &&
757 aBidiLevel > levelAfter // rule c11/12
758 &&
759 IS_SAME_DIRECTION(
760 levelBefore,
761 levelAfter) // before and after have the same parity
762 &&
763 !IS_SAME_DIRECTION(
764 aBidiLevel, levelAfter)) // caret has different parity
765 {
766 if (NS_SUCCEEDED(aFrameSelection->GetFrameFromLevel(
767 frameBefore, eDirPrevious, aBidiLevel, &theFrame))) {
768 theFrame->GetOffsets(start, end);
769 levelBefore = theFrame->GetEmbeddingLevel();
770 if (IS_LEVEL_RTL(aBidiLevel)) // c12: caret to the left of the
771 // leftmost character
772 theFrameOffset = IS_LEVEL_RTL(levelBefore) ? end : start;
773 else // c11: caret to the right of the rightmost character
774 theFrameOffset = IS_LEVEL_RTL(levelBefore) ? start : end;
775 }
776 }
777 }
778 }
779 }
780 }
781
782 *aReturnFrame = theFrame;
783 *aReturnOffset = theFrameOffset;
784 return NS_OK;
785 }
786
SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const787 size_t nsCaret::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
788 size_t total = aMallocSizeOf(this);
789 if (mPresShell) {
790 // We only want the size of the nsWeakReference object, not the PresShell
791 // (since we don't own the PresShell).
792 total += mPresShell->SizeOfOnlyThis(aMallocSizeOf);
793 }
794 if (mDomSelectionWeak) {
795 // We only want size of the nsWeakReference object, not the selection
796 // (again, we don't own the selection).
797 total += mDomSelectionWeak->SizeOfOnlyThis(aMallocSizeOf);
798 }
799 if (mBlinkTimer) {
800 total += mBlinkTimer->SizeOfIncludingThis(aMallocSizeOf);
801 }
802 return total;
803 }
804
IsMenuPopupHidingCaret()805 bool nsCaret::IsMenuPopupHidingCaret() {
806 #ifdef MOZ_XUL
807 // Check if there are open popups.
808 nsXULPopupManager* popMgr = nsXULPopupManager::GetInstance();
809 nsTArray<nsIFrame*> popups;
810 popMgr->GetVisiblePopups(popups);
811
812 if (popups.Length() == 0)
813 return false; // No popups, so caret can't be hidden by them.
814
815 // Get the selection focus content, that's where the caret would
816 // go if it was drawn.
817 nsCOMPtr<nsIDOMNode> node;
818 nsCOMPtr<nsISelection> domSelection = do_QueryReferent(mDomSelectionWeak);
819 if (!domSelection) return true; // No selection/caret to draw.
820 domSelection->GetFocusNode(getter_AddRefs(node));
821 if (!node) return true; // No selection/caret to draw.
822 nsCOMPtr<nsIContent> caretContent = do_QueryInterface(node);
823 if (!caretContent) return true; // No selection/caret to draw.
824
825 // If there's a menu popup open before the popup with
826 // the caret, don't show the caret.
827 for (uint32_t i = 0; i < popups.Length(); i++) {
828 nsMenuPopupFrame* popupFrame = static_cast<nsMenuPopupFrame*>(popups[i]);
829 nsIContent* popupContent = popupFrame->GetContent();
830
831 if (nsContentUtils::ContentIsDescendantOf(caretContent, popupContent)) {
832 // The caret is in this popup. There were no menu popups before this
833 // popup, so don't hide the caret.
834 return false;
835 }
836
837 if (popupFrame->PopupType() == ePopupTypeMenu &&
838 !popupFrame->IsContextMenu()) {
839 // This is an open menu popup. It does not contain the caret (else we'd
840 // have returned above). Even if the caret is in a subsequent popup,
841 // or another document/frame, it should be hidden.
842 return true;
843 }
844 }
845 #endif
846
847 // There are no open menu popups, no need to hide the caret.
848 return false;
849 }
850
ComputeCaretRects(nsIFrame * aFrame,int32_t aFrameOffset,nsRect * aCaretRect,nsRect * aHookRect)851 void nsCaret::ComputeCaretRects(nsIFrame* aFrame, int32_t aFrameOffset,
852 nsRect* aCaretRect, nsRect* aHookRect) {
853 NS_ASSERTION(aFrame, "Should have a frame here");
854
855 WritingMode wm = aFrame->GetWritingMode();
856 bool isVertical = wm.IsVertical();
857
858 nscoord bidiIndicatorSize;
859 *aCaretRect = GetGeometryForFrame(aFrame, aFrameOffset, &bidiIndicatorSize);
860
861 // on RTL frames the right edge of mCaretRect must be equal to framePos
862 const nsStyleVisibility* vis = aFrame->StyleVisibility();
863 if (NS_STYLE_DIRECTION_RTL == vis->mDirection) {
864 if (isVertical) {
865 aCaretRect->y -= aCaretRect->height;
866 } else {
867 aCaretRect->x -= aCaretRect->width;
868 }
869 }
870
871 // Simon -- make a hook to draw to the left or right of the caret to show
872 // keyboard language direction
873 aHookRect->SetEmpty();
874 if (!IsBidiUI()) {
875 return;
876 }
877
878 bool isCaretRTL;
879 nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard();
880 // if bidiKeyboard->IsLangRTL() fails, there is no way to tell the
881 // keyboard direction, or the user has no right-to-left keyboard
882 // installed, so we never draw the hook.
883 if (bidiKeyboard && NS_SUCCEEDED(bidiKeyboard->IsLangRTL(&isCaretRTL))) {
884 // If keyboard language is RTL, draw the hook on the left; if LTR, to the
885 // right The height of the hook rectangle is the same as the width of the
886 // caret rectangle.
887 if (isVertical) {
888 bool isSidewaysLR = wm.IsVerticalLR() && !wm.IsLineInverted();
889 if (isSidewaysLR) {
890 aHookRect->SetRect(aCaretRect->x + bidiIndicatorSize,
891 aCaretRect->y + (!isCaretRTL ? bidiIndicatorSize * -1
892 : aCaretRect->height),
893 aCaretRect->height, bidiIndicatorSize);
894 } else {
895 aHookRect->SetRect(aCaretRect->XMost() - bidiIndicatorSize,
896 aCaretRect->y + (isCaretRTL ? bidiIndicatorSize * -1
897 : aCaretRect->height),
898 aCaretRect->height, bidiIndicatorSize);
899 }
900 } else {
901 aHookRect->SetRect(aCaretRect->x + (isCaretRTL ? bidiIndicatorSize * -1
902 : aCaretRect->width),
903 aCaretRect->y + bidiIndicatorSize, bidiIndicatorSize,
904 aCaretRect->width);
905 }
906 }
907 }
908
909 /* static */
CaretBlinkCallback(nsITimer * aTimer,void * aClosure)910 void nsCaret::CaretBlinkCallback(nsITimer* aTimer, void* aClosure) {
911 nsCaret* theCaret = reinterpret_cast<nsCaret*>(aClosure);
912 if (!theCaret) {
913 return;
914 }
915 theCaret->mIsBlinkOn = !theCaret->mIsBlinkOn;
916 theCaret->SchedulePaint();
917
918 // mBlinkCount of -1 means blink count is not enabled.
919 if (theCaret->mBlinkCount == -1) {
920 return;
921 }
922
923 // Track the blink count, but only at end of a blink cycle.
924 if (!theCaret->mIsBlinkOn) {
925 // If we exceeded the blink count, stop the timer.
926 if (--theCaret->mBlinkCount <= 0) {
927 theCaret->StopBlinking();
928 }
929 }
930 }
931
SetIgnoreUserModify(bool aIgnoreUserModify)932 void nsCaret::SetIgnoreUserModify(bool aIgnoreUserModify) {
933 mIgnoreUserModify = aIgnoreUserModify;
934 SchedulePaint();
935 }
936