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/. */
7 #include "nsComboboxControlFrame.h"
9 #include "gfxContext.h"
12 #include "nsDeviceContext.h"
13 #include "nsFocusManager.h"
14 #include "nsGkAtoms.h"
15 #include "nsHTMLParts.h"
16 #include "nsIFormControl.h"
17 #include "nsILayoutHistoryState.h"
18 #include "nsListControlFrame.h"
19 #include "nsPIDOMWindow.h"
21 #include "nsViewManager.h"
22 #include "nsISelectControlFrame.h"
23 #include "nsContentUtils.h"
24 #include "mozilla/dom/Event.h"
25 #include "mozilla/dom/HTMLSelectElement.h"
26 #include "mozilla/dom/Document.h"
27 #include "nsIScrollableFrame.h"
28 #include "mozilla/ServoStyleSet.h"
29 #include "nsNodeInfoManager.h"
30 #include "nsContentCreatorFunctions.h"
31 #include "nsLayoutUtils.h"
32 #include "nsDisplayList.h"
34 #include "nsStyleConsts.h"
35 #include "nsTextFrameUtils.h"
36 #include "nsTextRunTransformations.h"
37 #include "HTMLSelectEventListener.h"
38 #include "mozilla/Likely.h"
40 #include "nsTextNode.h"
41 #include "mozilla/AsyncEventDispatcher.h"
42 #include "mozilla/LookAndFeel.h"
43 #include "mozilla/PresShell.h"
44 #include "mozilla/PresShellInlines.h"
46 using namespace mozilla
;
47 using namespace mozilla::gfx
;
50 nsComboboxControlFrame::RedisplayTextEvent::Run() {
51 if (mControlFrame
) mControlFrame
->HandleRedisplayTextEvent();
55 // Drop down list event management.
56 // The combo box uses the following strategy for managing the drop-down list.
57 // If the combo box or its arrow button is clicked on the drop-down list is
58 // displayed If mouse exits the combo box with the drop-down list displayed the
59 // drop-down list is asked to capture events The drop-down list will capture all
60 // events including mouse down and up and will always return with
61 // ListWasSelected method call regardless of whether an item in the list was
63 // The ListWasSelected code will turn off mouse-capture for the drop-down list.
64 // The drop-down list does not explicitly set capture when it is in the
67 nsComboboxControlFrame
* NS_NewComboboxControlFrame(PresShell
* aPresShell
,
68 ComputedStyle
* aStyle
) {
69 nsComboboxControlFrame
* it
= new (aPresShell
)
70 nsComboboxControlFrame(aStyle
, aPresShell
->GetPresContext());
74 NS_IMPL_FRAMEARENA_HELPERS(nsComboboxControlFrame
)
76 nsComboboxControlFrame::nsComboboxControlFrame(ComputedStyle
* aStyle
,
77 nsPresContext
* aPresContext
)
78 : nsHTMLButtonControlFrame(aStyle
, aPresContext
, kClassID
) {}
80 nsComboboxControlFrame::~nsComboboxControlFrame() = default;
82 NS_QUERYFRAME_HEAD(nsComboboxControlFrame
)
83 NS_QUERYFRAME_ENTRY(nsComboboxControlFrame
)
84 NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator
)
85 NS_QUERYFRAME_ENTRY(nsISelectControlFrame
)
86 NS_QUERYFRAME_TAIL_INHERITING(nsHTMLButtonControlFrame
)
89 a11y::AccType
nsComboboxControlFrame::AccessibleType() {
90 return a11y::eHTMLComboboxType
;
94 bool nsComboboxControlFrame::HasDropDownButton() const {
95 const nsStyleDisplay
* disp
= StyleDisplay();
96 // FIXME(emilio): Blink also shows this for menulist-button and such... Seems
97 // more similar to our mac / linux implementation.
98 return disp
->EffectiveAppearance() == StyleAppearance::Menulist
&&
100 PresContext()->Theme()->ThemeNeedsComboboxDropmarker());
103 nscoord
nsComboboxControlFrame::DropDownButtonISize() {
104 if (!HasDropDownButton()) {
108 nsPresContext
* pc
= PresContext();
109 LayoutDeviceIntSize dropdownButtonSize
= pc
->Theme()->GetMinimumWidgetSize(
110 pc
, this, StyleAppearance::MozMenulistArrowButton
);
111 return pc
->DevPixelsToAppUnits(dropdownButtonSize
.width
);
114 int32_t nsComboboxControlFrame::CharCountOfLargestOptionForInflation() const {
115 uint32_t maxLength
= 0;
117 for (auto i
: IntegerRange(Select().Options()->Length())) {
118 GetOptionText(i
, label
);
119 maxLength
= std::max(
121 nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression(
122 label
, StyleText()));
124 if (MOZ_UNLIKELY(maxLength
> uint32_t(INT32_MAX
))) {
127 return int32_t(maxLength
);
130 nscoord
nsComboboxControlFrame::GetLongestOptionISize(
131 gfxContext
* aRenderingContext
) const {
132 // Compute the width of each option's (potentially text-transformed) text,
133 // and use the widest one as part of our intrinsic size.
134 nscoord maxOptionSize
= 0;
136 nsAutoString transformedLabel
;
137 RefPtr
<nsFontMetrics
> fm
=
138 nsLayoutUtils::GetInflatedFontMetricsForFrame(this);
139 const nsStyleText
* textStyle
= StyleText();
140 auto textTransform
= textStyle
->mTextTransform
.IsNone()
142 : Some(textStyle
->mTextTransform
);
143 nsAtom
* language
= StyleFont()->mLanguage
;
144 AutoTArray
<bool, 50> charsToMergeArray
;
145 AutoTArray
<bool, 50> deletedCharsArray
;
146 for (auto i
: IntegerRange(Select().Options()->Length())) {
147 GetOptionText(i
, label
);
148 const nsAutoString
* stringToUse
= &label
;
150 textStyle
->mWebkitTextSecurity
!= StyleTextSecurity::None
) {
151 transformedLabel
.Truncate();
152 charsToMergeArray
.SetLengthAndRetainStorage(0);
153 deletedCharsArray
.SetLengthAndRetainStorage(0);
154 nsCaseTransformTextRunFactory::TransformString(
155 label
, transformedLabel
, textTransform
,
156 textStyle
->TextSecurityMaskChar(),
157 /* aCaseTransformsOnly = */ false, language
, charsToMergeArray
,
159 stringToUse
= &transformedLabel
;
161 maxOptionSize
= std::max(maxOptionSize
,
162 nsLayoutUtils::AppUnitWidthOfStringBidi(
163 *stringToUse
, this, *fm
, *aRenderingContext
));
166 // HACK: Add one app unit to workaround silly Netgear router styling, see
167 // bug 1769580. In practice since this comes from font metrics is unlikely
168 // to be perceivable.
171 return maxOptionSize
;
174 nscoord
nsComboboxControlFrame::GetIntrinsicISize(gfxContext
* aRenderingContext
,
175 IntrinsicISizeType aType
) {
176 Maybe
<nscoord
> containISize
= ContainIntrinsicISize(NS_UNCONSTRAINEDSIZE
);
177 if (containISize
&& *containISize
!= NS_UNCONSTRAINEDSIZE
) {
178 return *containISize
;
181 nscoord displayISize
= 0;
182 if (!containISize
&& !StyleContent()->mContent
.IsNone()) {
183 displayISize
+= GetLongestOptionISize(aRenderingContext
);
186 // Add room for the dropmarker button (if there is one).
187 displayISize
+= DropDownButtonISize();
191 nscoord
nsComboboxControlFrame::GetMinISize(gfxContext
* aRenderingContext
) {
193 DISPLAY_MIN_INLINE_SIZE(this, minISize
);
194 minISize
= GetIntrinsicISize(aRenderingContext
, IntrinsicISizeType::MinISize
);
198 nscoord
nsComboboxControlFrame::GetPrefISize(gfxContext
* aRenderingContext
) {
200 DISPLAY_PREF_INLINE_SIZE(this, prefISize
);
202 GetIntrinsicISize(aRenderingContext
, IntrinsicISizeType::PrefISize
);
206 dom::HTMLSelectElement
& nsComboboxControlFrame::Select() const {
207 return *static_cast<dom::HTMLSelectElement
*>(GetContent());
210 void nsComboboxControlFrame::GetOptionText(uint32_t aIndex
,
211 nsAString
& aText
) const {
213 if (Element
* el
= Select().Options()->GetElementAt(aIndex
)) {
214 static_cast<dom::HTMLOptionElement
*>(el
)->GetRenderedLabel(aText
);
218 void nsComboboxControlFrame::Reflow(nsPresContext
* aPresContext
,
219 ReflowOutput
& aDesiredSize
,
220 const ReflowInput
& aReflowInput
,
221 nsReflowStatus
& aStatus
) {
223 MOZ_ASSERT(aStatus
.IsEmpty(), "Caller should pass a fresh reflow status!");
224 // Constraints we try to satisfy:
226 // 1) Default inline size of button is the vertical scrollbar size
227 // 2) If the inline size of button is bigger than our inline size, set
228 // inline size of button to 0.
229 // 3) Default block size of button is block size of display area
230 // 4) Inline size of display area is whatever is left over from our
231 // inline size after allocating inline size for the button.
232 // Make sure the displayed text is the same as the selected option,
234 mDisplayedIndex
= Select().SelectedIndex();
236 // In dropped down mode the "selected index" is the hovered menu item,
237 // we want the last selected item which is |mDisplayedIndex| in this case.
240 WritingMode wm
= aReflowInput
.GetWritingMode();
242 // Check if the theme specifies a minimum size for the dropdown button
244 const nscoord buttonISize
= DropDownButtonISize();
245 const auto padding
= aReflowInput
.ComputedLogicalPadding(wm
);
247 // We ignore inline-end-padding (by adding it to our label box size) if we
248 // have a dropdown button, so that the button aligns with the end of the
250 mDisplayISize
= aReflowInput
.ComputedISize() - buttonISize
;
252 mDisplayISize
+= padding
.IEnd(wm
);
255 nsHTMLButtonControlFrame::Reflow(aPresContext
, aDesiredSize
, aReflowInput
,
259 void nsComboboxControlFrame::Init(nsIContent
* aContent
,
260 nsContainerFrame
* aParent
,
261 nsIFrame
* aPrevInFlow
) {
262 nsHTMLButtonControlFrame::Init(aContent
, aParent
, aPrevInFlow
);
264 mEventListener
= new HTMLSelectEventListener(
265 Select(), HTMLSelectEventListener::SelectType::Combobox
);
268 nsresult
nsComboboxControlFrame::RedisplaySelectedText() {
269 nsAutoScriptBlocker scriptBlocker
;
270 mDisplayedIndex
= Select().SelectedIndex();
271 return RedisplayText();
274 nsresult
nsComboboxControlFrame::RedisplayText() {
275 nsString previewValue
;
276 nsString
previousText(mDisplayedOptionTextOrPreview
);
278 Select().GetPreviewValue(previewValue
);
279 // Get the text to display
280 if (!previewValue
.IsEmpty()) {
281 mDisplayedOptionTextOrPreview
= previewValue
;
282 } else if (mDisplayedIndex
!= -1 && !StyleContent()->mContent
.IsNone()) {
283 GetOptionText(mDisplayedIndex
, mDisplayedOptionTextOrPreview
);
285 mDisplayedOptionTextOrPreview
.Truncate();
288 // Send reflow command because the new text maybe larger
290 if (!previousText
.Equals(mDisplayedOptionTextOrPreview
)) {
291 // Don't call ActuallyDisplayText(true) directly here since that could cause
292 // recursive frame construction. See bug 283117 and the comment in
293 // HandleRedisplayTextEvent() below.
295 // Revoke outstanding events to avoid out-of-order events which could mean
296 // displaying the wrong text.
297 mRedisplayTextEvent
.Revoke();
299 NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(),
300 "If we happen to run our redisplay event now, we might kill "
302 mRedisplayTextEvent
= new RedisplayTextEvent(this);
303 nsContentUtils::AddScriptRunner(mRedisplayTextEvent
.get());
308 void nsComboboxControlFrame::HandleRedisplayTextEvent() {
309 // First, make sure that the content model is up to date and we've constructed
310 // the frames for all our content in the right places. Otherwise they'll end
311 // up under the wrong insertion frame when we ActuallyDisplayText, since that
312 // flushes out the content sink by calling SetText on a DOM node with aNotify
313 // set to true. See bug 289730.
314 AutoWeakFrame
weakThis(this);
315 PresContext()->Document()->FlushPendingNotifications(
316 FlushType::ContentAndNotify
);
317 if (!weakThis
.IsAlive()) {
320 mRedisplayTextEvent
.Forget();
321 ActuallyDisplayText(true);
322 // Note: `this` might be dead here.
325 void nsComboboxControlFrame::ActuallyDisplayText(bool aNotify
) {
326 RefPtr
<dom::Text
> displayContent
= mDisplayLabel
->GetFirstChild()->AsText();
327 // Have to use a space character of some sort for line-block-size calculations
328 // to be right. Also, the space character must be zero-width in order for the
329 // inline-size calculations to be consistent between size-contained comboboxes
330 // vs. empty comboboxes.
332 // XXXdholbert Does this space need to be "non-breaking"? I'm not sure if it
333 // matters, but we previously had a comment here (added in 2002) saying "Have
334 // to use a non-breaking space for line-height calculations to be right". So
335 // I'll stick with a non-breaking space for now...
336 displayContent
->SetText(mDisplayedOptionTextOrPreview
.IsEmpty()
338 : mDisplayedOptionTextOrPreview
,
342 bool nsComboboxControlFrame::IsDroppedDown() const {
343 return Select().OpenInParentProcess();
346 //----------------------------------------------------------------------
347 // nsISelectControlFrame
348 //----------------------------------------------------------------------
350 nsComboboxControlFrame::DoneAddingChildren(bool aIsDone
) { return NS_OK
; }
353 nsComboboxControlFrame::AddOption(int32_t aIndex
) {
354 if (aIndex
<= mDisplayedIndex
) {
362 nsComboboxControlFrame::RemoveOption(int32_t aIndex
) {
363 if (Select().Options()->Length()) {
364 if (aIndex
< mDisplayedIndex
) {
366 } else if (aIndex
== mDisplayedIndex
) {
367 mDisplayedIndex
= 0; // IE6 compat
371 // If we removed the last option, we need to blank things out
372 mDisplayedIndex
= -1;
379 nsComboboxControlFrame::OnSetSelectedIndex(int32_t aOldIndex
,
381 nsAutoScriptBlocker scriptBlocker
;
382 mDisplayedIndex
= aNewIndex
;
386 // End nsISelectControlFrame
387 //----------------------------------------------------------------------
389 nsresult
nsComboboxControlFrame::HandleEvent(nsPresContext
* aPresContext
,
390 WidgetGUIEvent
* aEvent
,
391 nsEventStatus
* aEventStatus
) {
392 NS_ENSURE_ARG_POINTER(aEventStatus
);
394 if (nsEventStatus_eConsumeNoDefault
== *aEventStatus
) {
398 return nsHTMLButtonControlFrame::HandleEvent(aPresContext
, aEvent
,
402 nsresult
nsComboboxControlFrame::CreateAnonymousContent(
403 nsTArray
<ContentInfo
>& aElements
) {
404 dom::Document
* doc
= mContent
->OwnerDoc();
405 mDisplayLabel
= doc
->CreateHTMLElement(nsGkAtoms::label
);
408 RefPtr
<nsTextNode
> text
= doc
->CreateEmptyTextNode();
409 mDisplayLabel
->AppendChildTo(text
, false, IgnoreErrors());
412 // set the value of the text node
413 mDisplayedIndex
= Select().SelectedIndex();
414 if (mDisplayedIndex
!= -1) {
415 GetOptionText(mDisplayedIndex
, mDisplayedOptionTextOrPreview
);
417 ActuallyDisplayText(false);
419 aElements
.AppendElement(mDisplayLabel
);
420 if (HasDropDownButton()) {
421 mButtonContent
= mContent
->OwnerDoc()->CreateHTMLElement(nsGkAtoms::button
);
423 // This gives the button a reasonable height. This could be done via CSS
424 // instead, but relative font units like 1lh don't play very well with our
425 // font inflation implementation, so we do it this way instead.
426 RefPtr
<nsTextNode
> text
= doc
->CreateTextNode(u
"\ufeff"_ns
);
427 mButtonContent
->AppendChildTo(text
, false, IgnoreErrors());
429 // Make someone to listen to the button.
430 mButtonContent
->SetAttr(kNameSpaceID_None
, nsGkAtoms::type
, u
"button"_ns
,
432 // Set tabindex="-1" so that the button is not tabbable
433 mButtonContent
->SetAttr(kNameSpaceID_None
, nsGkAtoms::tabindex
, u
"-1"_ns
,
435 aElements
.AppendElement(mButtonContent
);
441 void nsComboboxControlFrame::AppendAnonymousContentTo(
442 nsTArray
<nsIContent
*>& aElements
, uint32_t aFilter
) {
444 aElements
.AppendElement(mDisplayLabel
);
447 if (mButtonContent
) {
448 aElements
.AppendElement(mButtonContent
);
454 class ComboboxLabelFrame final
: public nsBlockFrame
{
457 NS_DECL_FRAMEARENA_HELPERS(ComboboxLabelFrame
)
459 #ifdef DEBUG_FRAME_DUMP
460 nsresult
GetFrameName(nsAString
& aResult
) const final
{
461 return MakeFrameName(u
"ComboboxLabel"_ns
, aResult
);
465 void Reflow(nsPresContext
* aPresContext
, ReflowOutput
& aDesiredSize
,
466 const ReflowInput
& aReflowInput
, nsReflowStatus
& aStatus
) final
;
469 ComboboxLabelFrame(ComputedStyle
* aStyle
, nsPresContext
* aPresContext
)
470 : nsBlockFrame(aStyle
, aPresContext
, kClassID
) {}
473 NS_QUERYFRAME_HEAD(ComboboxLabelFrame
)
474 NS_QUERYFRAME_ENTRY(ComboboxLabelFrame
)
475 NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame
)
476 NS_IMPL_FRAMEARENA_HELPERS(ComboboxLabelFrame
)
478 void ComboboxLabelFrame::Reflow(nsPresContext
* aPresContext
,
479 ReflowOutput
& aDesiredSize
,
480 const ReflowInput
& aReflowInput
,
481 nsReflowStatus
& aStatus
) {
482 MOZ_ASSERT(aStatus
.IsEmpty(), "Caller should pass a fresh reflow status!");
484 const nsComboboxControlFrame
* combobox
=
485 do_QueryFrame(GetParent()->GetParent());
486 MOZ_ASSERT(combobox
, "Combobox's frame tree is wrong!");
487 MOZ_ASSERT(aReflowInput
.ComputedPhysicalBorderPadding() == nsMargin(),
488 "We shouldn't have border and padding in UA!");
490 ReflowInput
state(aReflowInput
);
491 state
.SetComputedISize(combobox
->mDisplayISize
);
492 nsBlockFrame::Reflow(aPresContext
, aDesiredSize
, state
, aStatus
);
493 aStatus
.Reset(); // this type of frame can't be split
496 } // namespace mozilla
498 nsIFrame
* NS_NewComboboxLabelFrame(PresShell
* aPresShell
,
499 ComputedStyle
* aStyle
) {
500 return new (aPresShell
)
501 ComboboxLabelFrame(aStyle
, aPresShell
->GetPresContext());
504 void nsComboboxControlFrame::Destroy(DestroyContext
& aContext
) {
505 // Revoke any pending RedisplayTextEvent
506 mRedisplayTextEvent
.Revoke();
507 mEventListener
->Detach();
509 aContext
.AddAnonymousContent(mDisplayLabel
.forget());
510 aContext
.AddAnonymousContent(mButtonContent
.forget());
511 nsHTMLButtonControlFrame::Destroy(aContext
);
514 //---------------------------------------------------------
515 // gets the content (an option) by index and then set it as
516 // being selected or not selected
517 //---------------------------------------------------------
519 nsComboboxControlFrame::OnOptionSelected(int32_t aIndex
, bool aSelected
) {
521 nsAutoScriptBlocker blocker
;
522 mDisplayedIndex
= aIndex
;
525 AutoWeakFrame
weakFrame(this);
526 RedisplaySelectedText();
527 if (weakFrame
.IsAlive()) {
528 FireValueChangeEvent(); // Fire after old option is unselected
534 void nsComboboxControlFrame::FireValueChangeEvent() {
535 // Fire ValueChange event to indicate data value of combo box has changed
536 // FIXME(emilio): This shouldn't be exposed to content.
537 nsContentUtils::AddScriptRunner(new AsyncEventDispatcher(
538 mContent
, u
"ValueChange"_ns
, CanBubble::eYes
, ChromeOnlyDispatch::eNo
));