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/. */
9 #include "nsUnicharUtils.h"
10 #include "nsListControlFrame.h"
11 #include "HTMLSelectEventListener.h"
12 #include "nsGkAtoms.h"
13 #include "nsComboboxControlFrame.h"
14 #include "nsFontMetrics.h"
15 #include "nsIScrollableFrame.h"
16 #include "nsCSSRendering.h"
17 #include "nsLayoutUtils.h"
18 #include "nsDisplayList.h"
19 #include "nsContentUtils.h"
20 #include "mozilla/Attributes.h"
21 #include "mozilla/dom/Event.h"
22 #include "mozilla/dom/HTMLOptGroupElement.h"
23 #include "mozilla/dom/HTMLOptionsCollection.h"
24 #include "mozilla/dom/HTMLSelectElement.h"
25 #include "mozilla/dom/MouseEvent.h"
26 #include "mozilla/dom/MouseEventBinding.h"
27 #include "mozilla/EventStateManager.h"
28 #include "mozilla/LookAndFeel.h"
29 #include "mozilla/MouseEvents.h"
30 #include "mozilla/Preferences.h"
31 #include "mozilla/PresShell.h"
32 #include "mozilla/StaticPrefs_browser.h"
33 #include "mozilla/StaticPrefs_ui.h"
34 #include "mozilla/TextEvents.h"
37 using namespace mozilla
;
38 using namespace mozilla::dom
;
41 nsListControlFrame
* nsListControlFrame::mFocused
= nullptr;
43 //---------------------------------------------------------
44 nsListControlFrame
* NS_NewListControlFrame(PresShell
* aPresShell
,
45 ComputedStyle
* aStyle
) {
46 nsListControlFrame
* it
=
47 new (aPresShell
) nsListControlFrame(aStyle
, aPresShell
->GetPresContext());
49 it
->AddStateBits(NS_FRAME_INDEPENDENT_SELECTION
);
54 NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame
)
56 nsListControlFrame::nsListControlFrame(ComputedStyle
* aStyle
,
57 nsPresContext
* aPresContext
)
58 : nsHTMLScrollFrame(aStyle
, aPresContext
, kClassID
, false),
59 mMightNeedSecondPass(false),
60 mHasPendingInterruptAtStartOfReflow(false),
61 mForceSelection(false) {
62 mChangesSinceDragStart
= false;
64 mIsAllContentHere
= false;
65 mIsAllFramesHere
= false;
66 mHasBeenInitialized
= false;
68 mPostChildrenLoadedReset
= false;
71 nsListControlFrame::~nsListControlFrame() = default;
73 Maybe
<nscoord
> nsListControlFrame::GetNaturalBaselineBOffset(
74 WritingMode aWM
, BaselineSharingGroup aBaselineGroup
,
75 BaselineExportContext
) const {
76 // Unlike scroll frames which we inherit from, we don't export a baseline.
79 // for Bug 47302 (remove this comment later)
80 void nsListControlFrame::Destroy(DestroyContext
& aContext
) {
81 // get the receiver interface from the browser button's content node
82 NS_ENSURE_TRUE_VOID(mContent
);
84 // Clear the frame pointer on our event listener, just in case the
85 // event listener can outlive the frame.
87 mEventListener
->Detach();
88 nsHTMLScrollFrame::Destroy(aContext
);
91 void nsListControlFrame::BuildDisplayList(nsDisplayListBuilder
* aBuilder
,
92 const nsDisplayListSet
& aLists
) {
93 // We allow visibility:hidden <select>s to contain visible options.
95 // Don't allow painting of list controls when painting is suppressed.
96 // XXX why do we need this here? we should never reach this. Maybe
97 // because these can have widgets? Hmm
98 if (aBuilder
->IsBackgroundOnly()) return;
100 DO_GLOBAL_REFLOW_COUNT_DSP("nsListControlFrame");
102 nsHTMLScrollFrame::BuildDisplayList(aBuilder
, aLists
);
105 HTMLOptionElement
* nsListControlFrame::GetCurrentOption() const {
106 return mEventListener
->GetCurrentOption();
110 * This is called by the SelectsAreaFrame, which is the same
111 * as the frame returned by GetOptionsContainer. It's the frame which is
113 * @param aPt the offset of this frame, relative to the rendering reference
116 void nsListControlFrame::PaintFocus(DrawTarget
* aDrawTarget
, nsPoint aPt
) {
117 if (mFocused
!= this) return;
119 nsIFrame
* containerFrame
= GetOptionsContainer();
120 if (!containerFrame
) return;
122 nsIFrame
* childframe
= nullptr;
123 nsCOMPtr
<nsIContent
> focusedContent
= GetCurrentOption();
124 if (focusedContent
) {
125 childframe
= focusedContent
->GetPrimaryFrame();
130 // get the child rect
131 fRect
= childframe
->GetRect();
132 // get it into our coordinates
133 fRect
.MoveBy(childframe
->GetParent()->GetOffsetTo(this));
135 float inflation
= nsLayoutUtils::FontSizeInflationFor(this);
136 fRect
.x
= fRect
.y
= 0;
137 if (GetWritingMode().IsVertical()) {
138 fRect
.width
= GetScrollPortRect().width
;
139 fRect
.height
= CalcFallbackRowBSize(inflation
);
141 fRect
.width
= CalcFallbackRowBSize(inflation
);
142 fRect
.height
= GetScrollPortRect().height
;
144 fRect
.MoveBy(containerFrame
->GetOffsetTo(this));
148 const auto* domOpt
= HTMLOptionElement::FromNodeOrNull(focusedContent
);
149 const bool isSelected
= domOpt
&& domOpt
->Selected();
151 // Set up back stop colors and then ask L&F service for the real colors
153 LookAndFeel::Color(isSelected
? LookAndFeel::ColorID::Selecteditemtext
154 : LookAndFeel::ColorID::Selecteditem
,
157 nsCSSRendering::PaintFocus(PresContext(), aDrawTarget
, fRect
, color
);
160 void nsListControlFrame::InvalidateFocus() {
161 if (mFocused
!= this) return;
163 nsIFrame
* containerFrame
= GetOptionsContainer();
164 if (containerFrame
) {
165 containerFrame
->InvalidateFrame();
169 NS_QUERYFRAME_HEAD(nsListControlFrame
)
170 NS_QUERYFRAME_ENTRY(nsIFormControlFrame
)
171 NS_QUERYFRAME_ENTRY(nsISelectControlFrame
)
172 NS_QUERYFRAME_ENTRY(nsListControlFrame
)
173 NS_QUERYFRAME_TAIL_INHERITING(nsHTMLScrollFrame
)
176 a11y::AccType
nsListControlFrame::AccessibleType() {
177 return a11y::eHTMLSelectListType
;
181 // Return true if we found at least one <option> or non-empty <optgroup> label
182 // that has a frame. aResult will be the maximum BSize of those.
183 static bool GetMaxRowBSize(nsIFrame
* aContainer
, WritingMode aWM
,
186 for (nsIFrame
* child
: aContainer
->PrincipalChildList()) {
187 if (child
->GetContent()->IsHTMLElement(nsGkAtoms::optgroup
)) {
188 // An optgroup; drill through any scroll frame and recurse. |inner| might
189 // be null here though if |inner| is an anonymous leaf frame of some sort.
190 auto inner
= child
->GetContentInsertionFrame();
191 if (inner
&& GetMaxRowBSize(inner
, aWM
, aResult
)) {
195 // an option or optgroup label
196 bool isOptGroupLabel
=
197 child
->Style()->IsPseudoElement() &&
198 aContainer
->GetContent()->IsHTMLElement(nsGkAtoms::optgroup
);
199 nscoord childBSize
= child
->BSize(aWM
);
200 // XXX bug 1499176: skip empty <optgroup> labels (zero bsize) for now
201 if (!isOptGroupLabel
|| childBSize
> nscoord(0)) {
203 *aResult
= std::max(childBSize
, *aResult
);
210 //-----------------------------------------------------------------
211 // Main Reflow for ListBox/Dropdown
212 //-----------------------------------------------------------------
214 nscoord
nsListControlFrame::CalcBSizeOfARow() {
215 // Calculate the block size in our writing mode of a single row in the
216 // listbox or dropdown list by using the tallest thing in the subtree,
217 // since there may be option groups in addition to option elements,
218 // either of which may be visible or invisible, may use different
221 if (GetContainSizeAxes().mBContained
||
222 !GetMaxRowBSize(GetOptionsContainer(), GetWritingMode(), &rowBSize
)) {
223 // We don't have any <option>s or <optgroup> labels with a frame.
224 // (Or we're size-contained in block axis, which has the same outcome for
226 float inflation
= nsLayoutUtils::FontSizeInflationFor(this);
227 rowBSize
= CalcFallbackRowBSize(inflation
);
232 nscoord
nsListControlFrame::GetPrefISize(gfxContext
* aRenderingContext
) {
234 DISPLAY_PREF_INLINE_SIZE(this, result
);
236 // Always add scrollbar inline sizes to the pref-inline-size of the
237 // scrolled content. Combobox frames depend on this happening in the
238 // dropdown, and standalone listboxes are overflow:scroll so they need
240 WritingMode wm
= GetWritingMode();
241 Maybe
<nscoord
> containISize
= ContainIntrinsicISize();
242 result
= containISize
? *containISize
243 : GetScrolledFrame()->GetPrefISize(aRenderingContext
);
244 LogicalMargin
scrollbarSize(wm
, GetDesiredScrollbarSizes());
245 result
= NSCoordSaturatingAdd(result
, scrollbarSize
.IStartEnd(wm
));
249 nscoord
nsListControlFrame::GetMinISize(gfxContext
* aRenderingContext
) {
251 DISPLAY_MIN_INLINE_SIZE(this, result
);
253 // Always add scrollbar inline sizes to the min-inline-size of the
254 // scrolled content. Combobox frames depend on this happening in the
255 // dropdown, and standalone listboxes are overflow:scroll so they need
257 WritingMode wm
= GetWritingMode();
258 Maybe
<nscoord
> containISize
= ContainIntrinsicISize();
259 result
= containISize
? *containISize
260 : GetScrolledFrame()->GetMinISize(aRenderingContext
);
261 LogicalMargin
scrollbarSize(wm
, GetDesiredScrollbarSizes());
262 result
+= scrollbarSize
.IStartEnd(wm
);
267 void nsListControlFrame::Reflow(nsPresContext
* aPresContext
,
268 ReflowOutput
& aDesiredSize
,
269 const ReflowInput
& aReflowInput
,
270 nsReflowStatus
& aStatus
) {
271 MOZ_ASSERT(aStatus
.IsEmpty(), "Caller should pass a fresh reflow status!");
272 NS_WARNING_ASSERTION(aReflowInput
.ComputedISize() != NS_UNCONSTRAINEDSIZE
,
273 "Must have a computed inline size");
277 mHasPendingInterruptAtStartOfReflow
= aPresContext
->HasPendingInterrupt();
279 // If all the content and frames are here
280 // then initialize it before reflow
281 if (mIsAllContentHere
&& !mHasBeenInitialized
) {
282 if (!mIsAllFramesHere
) {
283 CheckIfAllFramesHere();
285 if (mIsAllFramesHere
&& !mHasBeenInitialized
) {
286 mHasBeenInitialized
= true;
291 // Due to the fact that our intrinsic block size depends on the block
292 // sizes of our kids, we end up having to do two-pass reflow, in
293 // general -- the first pass to find the intrinsic block size and a
294 // second pass to reflow the scrollframe at that block size (which
295 // will size the scrollbars correctly, etc).
297 // Naturally, we want to avoid doing the second reflow as much as
298 // possible. We can skip it in the following cases (in all of which the first
299 // reflow is already happening at the right block size):
300 bool autoBSize
= (aReflowInput
.ComputedBSize() == NS_UNCONSTRAINEDSIZE
);
301 Maybe
<nscoord
> containBSize
= ContainIntrinsicBSize(NS_UNCONSTRAINEDSIZE
);
302 bool usingContainBSize
=
303 autoBSize
&& containBSize
&& *containBSize
!= NS_UNCONSTRAINEDSIZE
;
305 mMightNeedSecondPass
= [&] {
307 // We're reflowing with a constrained computed block size -- just use that
311 if (!IsSubtreeDirty() && !aReflowInput
.ShouldReflowAllKids()) {
312 // We're not dirty and have no dirty kids and shouldn't be reflowing all
313 // kids. In this case, our cached max block size of a child is not going
317 if (usingContainBSize
) {
318 // We're size-contained in the block axis. In this case the size of a row
319 // doesn't depend on our children (it's the "fallback" size).
322 // We might need to do a second pass. If we do our first reflow using our
323 // cached max block size of a child, then compute the new max block size,
324 // and it's the same as the old one, we might still skip it (see the
325 // IsScrollbarUpdateSuppressed() check).
329 ReflowInput
state(aReflowInput
);
330 int32_t length
= GetNumberOfRows();
332 nscoord oldBSizeOfARow
= BSizeOfARow();
334 if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW
) && autoBSize
) {
335 // When not doing an initial reflow, and when the block size is
336 // auto, start off with our computed block size set to what we'd
337 // expect our block size to be.
338 nscoord computedBSize
= CalcIntrinsicBSize(oldBSizeOfARow
, length
);
339 computedBSize
= state
.ApplyMinMaxBSize(computedBSize
);
340 state
.SetComputedBSize(computedBSize
);
343 if (usingContainBSize
) {
344 state
.SetComputedBSize(*containBSize
);
347 nsHTMLScrollFrame::Reflow(aPresContext
, aDesiredSize
, state
, aStatus
);
349 if (!mMightNeedSecondPass
) {
350 NS_ASSERTION(!autoBSize
|| BSizeOfARow() == oldBSizeOfARow
,
351 "How did our BSize of a row change if nothing was dirty?");
352 NS_ASSERTION(!autoBSize
|| !HasAnyStateBits(NS_FRAME_FIRST_REFLOW
) ||
354 "How do we not need a second pass during initial reflow at "
356 NS_ASSERTION(!IsScrollbarUpdateSuppressed(),
357 "Shouldn't be suppressing if we don't need a second pass!");
358 if (!autoBSize
|| usingContainBSize
) {
359 // Update our mNumDisplayRows based on our new row block size now
360 // that we know it. Note that if autoBSize and we landed in this
361 // code then we already set mNumDisplayRows in CalcIntrinsicBSize.
362 // Also note that we can't use BSizeOfARow() here because that
363 // just uses a cached value that we didn't compute.
364 nscoord rowBSize
= CalcBSizeOfARow();
366 // Just pick something
369 mNumDisplayRows
= std::max(1, state
.ComputedBSize() / rowBSize
);
376 mMightNeedSecondPass
= false;
378 // Now see whether we need a second pass. If we do, our
379 // nsSelectsAreaFrame will have suppressed the scrollbar update.
380 if (!IsScrollbarUpdateSuppressed()) {
381 // All done. No need to do more reflow.
385 SetSuppressScrollbarUpdate(false);
387 // Gotta reflow again.
388 // XXXbz We're just changing the block size here; do we need to dirty
389 // ourselves or anything like that? We might need to, per the letter
390 // of the reflow protocol, but things seem to work fine without it...
391 // Is that just an implementation detail of nsHTMLScrollFrame that
392 // we're depending on?
393 nsHTMLScrollFrame::DidReflow(aPresContext
, &state
);
395 // Now compute the block size we want to have
396 nscoord computedBSize
= CalcIntrinsicBSize(BSizeOfARow(), length
);
397 computedBSize
= state
.ApplyMinMaxBSize(computedBSize
);
398 state
.SetComputedBSize(computedBSize
);
400 // XXXbz to make the ascent really correct, we should add our
401 // mComputedPadding.top to it (and subtract it from descent). Need that
402 // because nsGfxScrollFrame just adds in the border....
404 nsHTMLScrollFrame::Reflow(aPresContext
, aDesiredSize
, state
, aStatus
);
407 bool nsListControlFrame::ShouldPropagateComputedBSizeToScrolledContent() const {
411 //---------------------------------------------------------
412 nsContainerFrame
* nsListControlFrame::GetContentInsertionFrame() {
413 return GetOptionsContainer()->GetContentInsertionFrame();
416 //---------------------------------------------------------
417 bool nsListControlFrame::ExtendedSelection(int32_t aStartIndex
,
418 int32_t aEndIndex
, bool aClearAll
) {
419 return SetOptionsSelectedFromFrame(aStartIndex
, aEndIndex
, true, aClearAll
);
422 //---------------------------------------------------------
423 bool nsListControlFrame::SingleSelection(int32_t aClickedIndex
,
426 nsCOMPtr
<nsIContent
> prevOption
= mEventListener
->GetCurrentOption();
428 bool wasChanged
= false;
429 // Get Current selection
431 wasChanged
= ToggleOptionSelectedFromFrame(aClickedIndex
);
434 SetOptionsSelectedFromFrame(aClickedIndex
, aClickedIndex
, true, true);
436 AutoWeakFrame
weakFrame(this);
437 ScrollToIndex(aClickedIndex
);
438 if (!weakFrame
.IsAlive()) {
442 mStartSelectionIndex
= aClickedIndex
;
443 mEndSelectionIndex
= aClickedIndex
;
447 FireMenuItemActiveEvent(prevOption
);
453 void nsListControlFrame::InitSelectionRange(int32_t aClickedIndex
) {
455 // If nothing is selected, set the start selection depending on where
456 // the user clicked and what the initial selection is:
457 // - if the user clicked *before* selectedIndex, set the start index to
458 // the end of the first contiguous selection.
459 // - if the user clicked *after* the end of the first contiguous
460 // selection, set the start index to selectedIndex.
461 // - if the user clicked *within* the first contiguous selection, set the
462 // start index to selectedIndex.
463 // The last two rules, of course, boil down to the same thing: if the user
464 // clicked >= selectedIndex, return selectedIndex.
466 // This makes it so that shift click works properly when you first click
467 // in a multiple select.
469 int32_t selectedIndex
= GetSelectedIndex();
470 if (selectedIndex
>= 0) {
471 // Get the end of the contiguous selection
472 RefPtr
<dom::HTMLOptionsCollection
> options
= GetOptions();
473 NS_ASSERTION(options
, "Collection of options is null!");
474 uint32_t numOptions
= options
->Length();
475 // Push i to one past the last selected index in the group.
477 for (i
= selectedIndex
+ 1; i
< numOptions
; i
++) {
478 if (!options
->ItemAsOption(i
)->Selected()) {
483 if (aClickedIndex
< selectedIndex
) {
484 // User clicked before selection, so start selection at end of
485 // contiguous selection
486 mStartSelectionIndex
= i
- 1;
487 mEndSelectionIndex
= selectedIndex
;
489 // User clicked after selection, so start selection at start of
490 // contiguous selection
491 mStartSelectionIndex
= selectedIndex
;
492 mEndSelectionIndex
= i
- 1;
497 static uint32_t CountOptionsAndOptgroups(nsIFrame
* aFrame
) {
499 for (nsIFrame
* child
: aFrame
->PrincipalChildList()) {
500 nsIContent
* content
= child
->GetContent();
502 if (content
->IsHTMLElement(nsGkAtoms::option
)) {
505 RefPtr
<HTMLOptGroupElement
> optgroup
=
506 HTMLOptGroupElement::FromNode(content
);
509 optgroup
->GetLabel(label
);
510 if (label
.Length() > 0) {
513 count
+= CountOptionsAndOptgroups(child
);
521 uint32_t nsListControlFrame::GetNumberOfRows() {
522 return ::CountOptionsAndOptgroups(GetContentInsertionFrame());
525 //---------------------------------------------------------
526 bool nsListControlFrame::PerformSelection(int32_t aClickedIndex
, bool aIsShift
,
528 bool wasChanged
= false;
530 if (aClickedIndex
== kNothingSelected
&& !mForceSelection
) {
531 // Ignore kNothingSelected unless the selection is forced
532 } else if (GetMultiple()) {
534 // Make sure shift+click actually does something expected when
535 // the user has never clicked on the select
536 if (mStartSelectionIndex
== kNothingSelected
) {
537 InitSelectionRange(aClickedIndex
);
540 // Get the range from beginning (low) to end (high)
541 // Shift *always* works, even if the current option is disabled
544 if (mStartSelectionIndex
== kNothingSelected
) {
545 startIndex
= aClickedIndex
;
546 endIndex
= aClickedIndex
;
547 } else if (mStartSelectionIndex
<= aClickedIndex
) {
548 startIndex
= mStartSelectionIndex
;
549 endIndex
= aClickedIndex
;
551 startIndex
= aClickedIndex
;
552 endIndex
= mStartSelectionIndex
;
555 // Clear only if control was not pressed
556 wasChanged
= ExtendedSelection(startIndex
, endIndex
, !aIsControl
);
557 AutoWeakFrame
weakFrame(this);
558 ScrollToIndex(aClickedIndex
);
559 if (!weakFrame
.IsAlive()) {
563 if (mStartSelectionIndex
== kNothingSelected
) {
564 mStartSelectionIndex
= aClickedIndex
;
567 nsCOMPtr
<nsIContent
> prevOption
= GetCurrentOption();
569 mEndSelectionIndex
= aClickedIndex
;
573 FireMenuItemActiveEvent(prevOption
);
575 } else if (aIsControl
) {
576 wasChanged
= SingleSelection(aClickedIndex
, true); // might destroy us
578 wasChanged
= SingleSelection(aClickedIndex
, false); // might destroy us
581 wasChanged
= SingleSelection(aClickedIndex
, false); // might destroy us
587 //---------------------------------------------------------
588 bool nsListControlFrame::HandleListSelection(dom::Event
* aEvent
,
589 int32_t aClickedIndex
) {
590 MouseEvent
* mouseEvent
= aEvent
->AsMouseEvent();
593 isControl
= mouseEvent
->MetaKey();
595 isControl
= mouseEvent
->CtrlKey();
597 bool isShift
= mouseEvent
->ShiftKey();
598 return PerformSelection(aClickedIndex
, isShift
,
599 isControl
); // might destroy us
602 //---------------------------------------------------------
603 void nsListControlFrame::CaptureMouseEvents(bool aGrabMouseEvents
) {
604 if (aGrabMouseEvents
) {
605 PresShell::SetCapturingContent(mContent
, CaptureFlags::IgnoreAllowedState
);
607 nsIContent
* capturingContent
= PresShell::GetCapturingContent();
608 if (capturingContent
== mContent
) {
609 // only clear the capturing content if *we* are the ones doing the
610 // capturing (or if the dropdown is hidden, in which case NO-ONE should
611 // be capturing anything - it could be a scrollbar inside this listbox
612 // which is actually grabbing
613 // This shouldn't be necessary. We should simply ensure that events
614 // targeting scrollbars are never visible to DOM consumers.
615 PresShell::ReleaseCapturingContent();
620 //---------------------------------------------------------
621 nsresult
nsListControlFrame::HandleEvent(nsPresContext
* aPresContext
,
622 WidgetGUIEvent
* aEvent
,
623 nsEventStatus
* aEventStatus
) {
624 NS_ENSURE_ARG_POINTER(aEventStatus
);
626 /*const char * desc[] = {"eMouseMove",
627 "NS_MOUSE_LEFT_BUTTON_UP",
628 "NS_MOUSE_LEFT_BUTTON_DOWN",
629 "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
630 "NS_MOUSE_MIDDLE_BUTTON_UP",
631 "NS_MOUSE_MIDDLE_BUTTON_DOWN",
632 "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
633 "NS_MOUSE_RIGHT_BUTTON_UP",
634 "NS_MOUSE_RIGHT_BUTTON_DOWN",
637 "NS_MOUSE_LEFT_DOUBLECLICK",
638 "NS_MOUSE_MIDDLE_DOUBLECLICK",
639 "NS_MOUSE_RIGHT_DOUBLECLICK",
640 "NS_MOUSE_LEFT_CLICK",
641 "NS_MOUSE_MIDDLE_CLICK",
642 "NS_MOUSE_RIGHT_CLICK"};
643 int inx = aEvent->mMessage - eMouseEventFirst;
644 if (inx >= 0 && inx <= (NS_MOUSE_RIGHT_CLICK - eMouseEventFirst)) {
645 printf("Mouse in ListFrame %s [%d]\n", desc[inx], aEvent->mMessage);
647 printf("Mouse in ListFrame <UNKNOWN> [%d]\n", aEvent->mMessage);
650 if (nsEventStatus_eConsumeNoDefault
== *aEventStatus
) return NS_OK
;
652 // disabled state affects how we're selected, but we don't want to go through
653 // nsHTMLScrollFrame if we're disabled.
654 if (IsContentDisabled()) {
655 return nsIFrame::HandleEvent(aPresContext
, aEvent
, aEventStatus
);
658 return nsHTMLScrollFrame::HandleEvent(aPresContext
, aEvent
, aEventStatus
);
661 //---------------------------------------------------------
662 void nsListControlFrame::SetInitialChildList(ChildListID aListID
,
663 nsFrameList
&& aChildList
) {
664 if (aListID
== FrameChildListID::Principal
) {
665 // First check to see if all the content has been added
666 mIsAllContentHere
= Select().IsDoneAddingChildren();
667 if (!mIsAllContentHere
) {
668 mIsAllFramesHere
= false;
669 mHasBeenInitialized
= false;
672 nsHTMLScrollFrame::SetInitialChildList(aListID
, std::move(aChildList
));
675 HTMLSelectElement
& nsListControlFrame::Select() const {
676 return *static_cast<HTMLSelectElement
*>(GetContent());
679 //---------------------------------------------------------
680 void nsListControlFrame::Init(nsIContent
* aContent
, nsContainerFrame
* aParent
,
681 nsIFrame
* aPrevInFlow
) {
682 nsHTMLScrollFrame::Init(aContent
, aParent
, aPrevInFlow
);
684 // we shouldn't have to unregister this listener because when
685 // our frame goes away all these content node go away as well
686 // because our frame is the only one who references them.
687 // we need to hook up our listeners before the editor is initialized
688 mEventListener
= new HTMLSelectEventListener(
689 Select(), HTMLSelectEventListener::SelectType::Listbox
);
691 mStartSelectionIndex
= kNothingSelected
;
692 mEndSelectionIndex
= kNothingSelected
;
695 dom::HTMLOptionsCollection
* nsListControlFrame::GetOptions() const {
696 return Select().Options();
699 dom::HTMLOptionElement
* nsListControlFrame::GetOption(uint32_t aIndex
) const {
700 return Select().Item(aIndex
);
704 nsListControlFrame::OnOptionSelected(int32_t aIndex
, bool aSelected
) {
706 ScrollToIndex(aIndex
);
711 void nsListControlFrame::OnContentReset() { ResetList(true); }
713 void nsListControlFrame::ResetList(bool aAllowScrolling
) {
714 // if all the frames aren't here
715 // don't bother reseting
716 if (!mIsAllFramesHere
) {
720 if (aAllowScrolling
) {
721 mPostChildrenLoadedReset
= true;
723 // Scroll to the selected index
724 int32_t indexToSelect
= kNothingSelected
;
726 HTMLSelectElement
* selectElement
= HTMLSelectElement::FromNode(mContent
);
728 indexToSelect
= selectElement
->SelectedIndex();
729 AutoWeakFrame
weakFrame(this);
730 ScrollToIndex(indexToSelect
);
731 if (!weakFrame
.IsAlive()) {
737 mStartSelectionIndex
= kNothingSelected
;
738 mEndSelectionIndex
= kNothingSelected
;
740 // Combobox will redisplay itself with the OnOptionSelected event
743 void nsListControlFrame::SetFocus(bool aOn
, bool aRepaint
) {
755 void nsListControlFrame::GetOptionText(uint32_t aIndex
, nsAString
& aStr
) {
757 if (dom::HTMLOptionElement
* optionElement
= GetOption(aIndex
)) {
758 optionElement
->GetRenderedLabel(aStr
);
762 int32_t nsListControlFrame::GetSelectedIndex() {
763 dom::HTMLSelectElement
* select
=
764 dom::HTMLSelectElement::FromNodeOrNull(mContent
);
765 return select
->SelectedIndex();
768 uint32_t nsListControlFrame::GetNumberOfOptions() {
769 dom::HTMLOptionsCollection
* options
= GetOptions();
774 return options
->Length();
777 //----------------------------------------------------------------------
778 // nsISelectControlFrame
779 //----------------------------------------------------------------------
780 bool nsListControlFrame::CheckIfAllFramesHere() {
781 // XXX Need to find a fail proof way to determine that
782 // all the frames are there
783 mIsAllFramesHere
= true;
785 // now make sure we have a frame each piece of content
787 return mIsAllFramesHere
;
791 nsListControlFrame::DoneAddingChildren(bool aIsDone
) {
792 mIsAllContentHere
= aIsDone
;
793 if (mIsAllContentHere
) {
794 // Here we check to see if all the frames have been created
795 // for all the content.
796 // If so, then we can initialize;
797 if (!mIsAllFramesHere
) {
798 // if all the frames are now present we can initialize
799 if (CheckIfAllFramesHere()) {
800 mHasBeenInitialized
= true;
809 nsListControlFrame::AddOption(int32_t aIndex
) {
810 #ifdef DO_REFLOW_DEBUG
811 printf("---- Id: %d nsLCF %p Added Option %d\n", mReflowId
, this, aIndex
);
814 if (!mIsAllContentHere
) {
815 mIsAllContentHere
= Select().IsDoneAddingChildren();
816 if (!mIsAllContentHere
) {
817 mIsAllFramesHere
= false;
818 mHasBeenInitialized
= false;
821 (aIndex
== static_cast<int32_t>(GetNumberOfOptions() - 1));
825 // Make sure we scroll to the selected option as needed
828 if (!mHasBeenInitialized
) {
832 mPostChildrenLoadedReset
= mIsAllContentHere
;
836 static int32_t DecrementAndClamp(int32_t aSelectionIndex
, int32_t aLength
) {
837 return aLength
== 0 ? nsListControlFrame::kNothingSelected
838 : std::max(0, aSelectionIndex
- 1);
842 nsListControlFrame::RemoveOption(int32_t aIndex
) {
843 MOZ_ASSERT(aIndex
>= 0, "negative <option> index");
845 // Need to reset if we're a dropdown
846 if (mStartSelectionIndex
!= kNothingSelected
) {
847 NS_ASSERTION(mEndSelectionIndex
!= kNothingSelected
, "");
848 int32_t numOptions
= GetNumberOfOptions();
849 // NOTE: numOptions is the new number of options whereas aIndex is the
850 // unadjusted index of the removed option (hence the <= below).
851 NS_ASSERTION(aIndex
<= numOptions
, "out-of-bounds <option> index");
853 int32_t forward
= mEndSelectionIndex
- mStartSelectionIndex
;
854 int32_t* low
= forward
>= 0 ? &mStartSelectionIndex
: &mEndSelectionIndex
;
855 int32_t* high
= forward
>= 0 ? &mEndSelectionIndex
: &mStartSelectionIndex
;
856 if (aIndex
< *low
) *low
= ::DecrementAndClamp(*low
, numOptions
);
857 if (aIndex
<= *high
) *high
= ::DecrementAndClamp(*high
, numOptions
);
858 if (forward
== 0) *low
= *high
;
860 NS_ASSERTION(mEndSelectionIndex
== kNothingSelected
, "");
866 //---------------------------------------------------------
867 // Set the option selected in the DOM. This method is named
868 // as it is because it indicates that the frame is the source
869 // of this event rather than the receiver.
870 bool nsListControlFrame::SetOptionsSelectedFromFrame(int32_t aStartIndex
,
874 using OptionFlag
= HTMLSelectElement::OptionFlag
;
875 RefPtr
<HTMLSelectElement
> selectElement
=
876 HTMLSelectElement::FromNode(mContent
);
878 HTMLSelectElement::OptionFlags mask
= OptionFlag::Notify
;
879 if (mForceSelection
) {
880 mask
+= OptionFlag::SetDisabled
;
883 mask
+= OptionFlag::IsSelected
;
886 mask
+= OptionFlag::ClearAll
;
889 return selectElement
->SetOptionsSelectedByIndex(aStartIndex
, aEndIndex
, mask
);
892 bool nsListControlFrame::ToggleOptionSelectedFromFrame(int32_t aIndex
) {
893 RefPtr
<HTMLOptionElement
> option
= GetOption(static_cast<uint32_t>(aIndex
));
894 NS_ENSURE_TRUE(option
, false);
896 RefPtr
<HTMLSelectElement
> selectElement
=
897 HTMLSelectElement::FromNode(mContent
);
899 HTMLSelectElement::OptionFlags mask
= HTMLSelectElement::OptionFlag::Notify
;
900 if (!option
->Selected()) {
901 mask
+= HTMLSelectElement::OptionFlag::IsSelected
;
904 return selectElement
->SetOptionsSelectedByIndex(aIndex
, aIndex
, mask
);
907 // Dispatch event and such
908 bool nsListControlFrame::UpdateSelection() {
909 if (mIsAllFramesHere
) {
910 // if it's a combobox, display the new text. Note that after
911 // FireOnInputAndOnChange we might be dead, as that can run script.
912 AutoWeakFrame
weakFrame(this);
913 if (mIsAllContentHere
) {
914 RefPtr listener
= mEventListener
;
915 listener
->FireOnInputAndOnChange();
917 return weakFrame
.IsAlive();
923 nsListControlFrame::OnSetSelectedIndex(int32_t aOldIndex
, int32_t aNewIndex
) {
925 nsCOMPtr
<nsIContent
> prevOption
= GetCurrentOption();
928 AutoWeakFrame
weakFrame(this);
929 ScrollToIndex(aNewIndex
);
930 if (!weakFrame
.IsAlive()) {
933 mStartSelectionIndex
= aNewIndex
;
934 mEndSelectionIndex
= aNewIndex
;
938 if (aOldIndex
!= aNewIndex
) {
939 FireMenuItemActiveEvent(prevOption
);
944 //----------------------------------------------------------------------
945 // End nsISelectControlFrame
946 //----------------------------------------------------------------------
948 class AsyncReset final
: public Runnable
{
950 AsyncReset(nsListControlFrame
* aFrame
, bool aScroll
)
951 : Runnable("AsyncReset"), mFrame(aFrame
), mScroll(aScroll
) {}
953 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD
Run() override
{
954 if (mFrame
.IsAlive()) {
955 static_cast<nsListControlFrame
*>(mFrame
.GetFrame())->ResetList(mScroll
);
965 nsresult
nsListControlFrame::SetFormProperty(nsAtom
* aName
,
966 const nsAString
& aValue
) {
967 if (nsGkAtoms::selected
== aName
) {
968 return NS_ERROR_INVALID_ARG
; // Selected is readonly according to spec.
969 } else if (nsGkAtoms::selectedindex
== aName
) {
970 // You shouldn't be calling me for this!!!
971 return NS_ERROR_INVALID_ARG
;
974 // We should be told about selectedIndex by the DOM element through
980 void nsListControlFrame::DidReflow(nsPresContext
* aPresContext
,
981 const ReflowInput
* aReflowInput
) {
982 bool wasInterrupted
= !mHasPendingInterruptAtStartOfReflow
&&
983 aPresContext
->HasPendingInterrupt();
985 nsHTMLScrollFrame::DidReflow(aPresContext
, aReflowInput
);
987 if (mNeedToReset
&& !wasInterrupted
) {
988 mNeedToReset
= false;
989 // Suppress scrolling to the selected element if we restored scroll
990 // history state AND the list contents have not changed since we loaded
991 // all the children AND nothing else forced us to scroll by calling
992 // ResetList(true). The latter two conditions are folded into
993 // mPostChildrenLoadedReset.
995 // The idea is that we want scroll history restoration to trump ResetList
996 // scrolling to the selected element, when the ResetList was probably only
997 // caused by content loading normally.
998 const bool scroll
= !DidHistoryRestore() || mPostChildrenLoadedReset
;
999 nsContentUtils::AddScriptRunner(new AsyncReset(this, scroll
));
1002 mHasPendingInterruptAtStartOfReflow
= false;
1005 #ifdef DEBUG_FRAME_DUMP
1006 nsresult
nsListControlFrame::GetFrameName(nsAString
& aResult
) const {
1007 return MakeFrameName(u
"ListControl"_ns
, aResult
);
1011 nscoord
nsListControlFrame::GetBSizeOfARow() { return BSizeOfARow(); }
1013 bool nsListControlFrame::IsOptionInteractivelySelectable(int32_t aIndex
) const {
1014 auto& select
= Select();
1015 if (HTMLOptionElement
* item
= select
.Item(aIndex
)) {
1016 return IsOptionInteractivelySelectable(&select
, item
);
1021 bool nsListControlFrame::IsOptionInteractivelySelectable(
1022 HTMLSelectElement
* aSelect
, HTMLOptionElement
* aOption
) {
1023 return !aSelect
->IsOptionDisabled(aOption
) && aOption
->GetPrimaryFrame();
1026 nscoord
nsListControlFrame::CalcFallbackRowBSize(float aFontSizeInflation
) {
1027 RefPtr
<nsFontMetrics
> fontMet
=
1028 nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation
);
1029 return fontMet
->MaxHeight();
1032 nscoord
nsListControlFrame::CalcIntrinsicBSize(nscoord aBSizeOfARow
,
1033 int32_t aNumberOfOptions
) {
1034 mNumDisplayRows
= Select().Size();
1035 if (mNumDisplayRows
< 1) {
1036 mNumDisplayRows
= 4;
1038 return mNumDisplayRows
* aBSizeOfARow
;
1041 #ifdef ACCESSIBILITY
1042 void nsListControlFrame::FireMenuItemActiveEvent(nsIContent
* aPreviousOption
) {
1043 if (mFocused
!= this) {
1047 nsIContent
* optionContent
= GetCurrentOption();
1048 if (aPreviousOption
== optionContent
) {
1053 if (aPreviousOption
) {
1054 FireDOMEvent(u
"DOMMenuItemInactive"_ns
, aPreviousOption
);
1057 if (optionContent
) {
1058 FireDOMEvent(u
"DOMMenuItemActive"_ns
, optionContent
);
1063 nsresult
nsListControlFrame::GetIndexFromDOMEvent(dom::Event
* aMouseEvent
,
1064 int32_t& aCurIndex
) {
1065 if (PresShell::GetCapturingContent() != mContent
) {
1066 // If we're not capturing, then ignore movement in the border
1068 nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aMouseEvent
, this);
1069 nsRect borderInnerEdge
= GetScrollPortRect();
1070 if (!borderInnerEdge
.Contains(pt
)) {
1071 return NS_ERROR_FAILURE
;
1075 RefPtr
<dom::HTMLOptionElement
> option
;
1076 for (nsCOMPtr
<nsIContent
> content
=
1077 PresContext()->EventStateManager()->GetEventTargetContent(nullptr);
1078 content
&& !option
; content
= content
->GetParent()) {
1079 option
= dom::HTMLOptionElement::FromNode(content
);
1083 aCurIndex
= option
->Index();
1084 MOZ_ASSERT(aCurIndex
>= 0);
1088 return NS_ERROR_FAILURE
;
1091 nsresult
nsListControlFrame::HandleLeftButtonMouseDown(
1092 dom::Event
* aMouseEvent
) {
1093 int32_t selectedIndex
;
1094 if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent
, selectedIndex
))) {
1096 CaptureMouseEvents(true);
1097 AutoWeakFrame
weakFrame(this);
1099 HandleListSelection(aMouseEvent
, selectedIndex
); // might destroy us
1100 if (!weakFrame
.IsAlive()) {
1103 mChangesSinceDragStart
= change
;
1108 nsresult
nsListControlFrame::HandleLeftButtonMouseUp(dom::Event
* aMouseEvent
) {
1109 if (!StyleVisibility()->IsVisible()) {
1113 if (mChangesSinceDragStart
) {
1114 // reset this so that future MouseUps without a prior MouseDown
1115 // won't fire onchange
1116 mChangesSinceDragStart
= false;
1117 RefPtr listener
= mEventListener
;
1118 listener
->FireOnInputAndOnChange();
1119 // Note that `this` may be dead now, as the above call runs script.
1124 nsresult
nsListControlFrame::DragMove(dom::Event
* aMouseEvent
) {
1125 NS_ASSERTION(aMouseEvent
, "aMouseEvent is null.");
1127 int32_t selectedIndex
;
1128 if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent
, selectedIndex
))) {
1129 // Don't waste cycles if we already dragged over this item
1130 if (selectedIndex
== mEndSelectionIndex
) {
1133 MouseEvent
* mouseEvent
= aMouseEvent
->AsMouseEvent();
1134 NS_ASSERTION(mouseEvent
, "aMouseEvent is not a MouseEvent!");
1137 isControl
= mouseEvent
->MetaKey();
1139 isControl
= mouseEvent
->CtrlKey();
1141 AutoWeakFrame
weakFrame(this);
1142 // Turn SHIFT on when you are dragging, unless control is on.
1143 bool wasChanged
= PerformSelection(selectedIndex
, !isControl
, isControl
);
1144 if (!weakFrame
.IsAlive()) {
1147 mChangesSinceDragStart
= mChangesSinceDragStart
|| wasChanged
;
1152 //----------------------------------------------------------------------
1154 //----------------------------------------------------------------------
1155 void nsListControlFrame::ScrollToIndex(int32_t aIndex
) {
1157 // XXX shouldn't we just do nothing if we're asked to scroll to
1158 // kNothingSelected?
1159 ScrollTo(nsPoint(0, 0), ScrollMode::Instant
);
1161 RefPtr
<dom::HTMLOptionElement
> option
=
1162 GetOption(AssertedCast
<uint32_t>(aIndex
));
1164 ScrollToFrame(*option
);
1169 void nsListControlFrame::ScrollToFrame(dom::HTMLOptionElement
& aOptElement
) {
1170 // otherwise we find the content's frame and scroll to it
1171 if (nsIFrame
* childFrame
= aOptElement
.GetPrimaryFrame()) {
1172 RefPtr
<mozilla::PresShell
> presShell
= PresShell();
1173 presShell
->ScrollFrameIntoView(childFrame
, Nothing(), ScrollAxis(),
1175 ScrollFlags::ScrollOverflowHidden
|
1176 ScrollFlags::ScrollFirstAncestorOnly
);
1180 void nsListControlFrame::UpdateSelectionAfterKeyEvent(
1181 int32_t aNewIndex
, uint32_t aCharCode
, bool aIsShift
, bool aIsControlOrMeta
,
1182 bool aIsControlSelectMode
) {
1183 // If you hold control, but not shift, no key will actually do anything
1185 AutoWeakFrame
weakFrame(this);
1186 bool wasChanged
= false;
1187 if (aIsControlOrMeta
&& !aIsShift
&& aCharCode
!= ' ') {
1188 #ifdef ACCESSIBILITY
1189 nsCOMPtr
<nsIContent
> prevOption
= GetCurrentOption();
1191 mStartSelectionIndex
= aNewIndex
;
1192 mEndSelectionIndex
= aNewIndex
;
1194 ScrollToIndex(aNewIndex
);
1195 if (!weakFrame
.IsAlive()) {
1199 #ifdef ACCESSIBILITY
1200 FireMenuItemActiveEvent(prevOption
);
1202 } else if (aIsControlSelectMode
&& aCharCode
== ' ') {
1203 wasChanged
= SingleSelection(aNewIndex
, true);
1205 wasChanged
= PerformSelection(aNewIndex
, aIsShift
, aIsControlOrMeta
);
1207 if (wasChanged
&& weakFrame
.IsAlive()) {
1208 // dispatch event, update combobox, etc.