no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / layout / forms / nsComboboxControlFrame.cpp
blob1d4ff15b4f5d0e4996f20afed671a7d2acdc8090
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"
10 #include "gfxUtils.h"
11 #include "nsCOMPtr.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"
20 #include "nsView.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"
33 #include "nsITheme.h"
34 #include "nsStyleConsts.h"
35 #include "nsTextFrameUtils.h"
36 #include "nsTextRunTransformations.h"
37 #include "HTMLSelectEventListener.h"
38 #include "mozilla/Likely.h"
39 #include <algorithm>
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;
49 NS_IMETHODIMP
50 nsComboboxControlFrame::RedisplayTextEvent::Run() {
51 if (mControlFrame) mControlFrame->HandleRedisplayTextEvent();
52 return NS_OK;
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
62 // actually selected.
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
65 // drop-down mode.
67 nsComboboxControlFrame* NS_NewComboboxControlFrame(PresShell* aPresShell,
68 ComputedStyle* aStyle) {
69 nsComboboxControlFrame* it = new (aPresShell)
70 nsComboboxControlFrame(aStyle, aPresShell->GetPresContext());
71 return it;
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)
88 #ifdef ACCESSIBILITY
89 a11y::AccType nsComboboxControlFrame::AccessibleType() {
90 return a11y::eHTMLComboboxType;
92 #endif
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 &&
99 (!IsThemed(disp) ||
100 PresContext()->Theme()->ThemeNeedsComboboxDropmarker());
103 nscoord nsComboboxControlFrame::DropDownButtonISize() {
104 if (!HasDropDownButton()) {
105 return 0;
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;
116 nsAutoString label;
117 for (auto i : IntegerRange(Select().Options()->Length())) {
118 GetOptionText(i, label);
119 maxLength = std::max(
120 maxLength,
121 nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression(
122 label, StyleText()));
124 if (MOZ_UNLIKELY(maxLength > uint32_t(INT32_MAX))) {
125 return 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;
135 nsAutoString label;
136 nsAutoString transformedLabel;
137 RefPtr<nsFontMetrics> fm =
138 nsLayoutUtils::GetInflatedFontMetricsForFrame(this);
139 const nsStyleText* textStyle = StyleText();
140 auto textTransform = textStyle->mTextTransform.IsNone()
141 ? Nothing()
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;
149 if (textTransform ||
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,
158 deletedCharsArray);
159 stringToUse = &transformedLabel;
161 maxOptionSize = std::max(maxOptionSize,
162 nsLayoutUtils::AppUnitWidthOfStringBidi(
163 *stringToUse, this, *fm, *aRenderingContext));
165 if (maxOptionSize) {
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.
169 maxOptionSize += 1;
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();
188 return displayISize;
191 nscoord nsComboboxControlFrame::GetMinISize(gfxContext* aRenderingContext) {
192 nscoord minISize;
193 DISPLAY_MIN_INLINE_SIZE(this, minISize);
194 minISize = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::MinISize);
195 return minISize;
198 nscoord nsComboboxControlFrame::GetPrefISize(gfxContext* aRenderingContext) {
199 nscoord prefISize;
200 DISPLAY_PREF_INLINE_SIZE(this, prefISize);
201 prefISize =
202 GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::PrefISize);
203 return 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 {
212 aText.Truncate();
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) {
222 MarkInReflow();
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,
233 // bug 297389.
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.
238 RedisplayText();
240 WritingMode wm = aReflowInput.GetWritingMode();
242 // Check if the theme specifies a minimum size for the dropdown button
243 // first.
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
249 // padding box.
250 mDisplayISize = aReflowInput.ComputedISize() - buttonISize;
251 if (buttonISize) {
252 mDisplayISize += padding.IEnd(wm);
255 nsHTMLButtonControlFrame::Reflow(aPresContext, aDesiredSize, aReflowInput,
256 aStatus);
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);
284 } else {
285 mDisplayedOptionTextOrPreview.Truncate();
288 // Send reflow command because the new text maybe larger
289 nsresult rv = NS_OK;
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 "
301 "ourselves!");
302 mRedisplayTextEvent = new RedisplayTextEvent(this);
303 nsContentUtils::AddScriptRunner(mRedisplayTextEvent.get());
305 return rv;
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()) {
318 return;
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()
337 ? u"\ufeff"_ns
338 : mDisplayedOptionTextOrPreview,
339 aNotify);
342 bool nsComboboxControlFrame::IsDroppedDown() const {
343 return Select().OpenInParentProcess();
346 //----------------------------------------------------------------------
347 // nsISelectControlFrame
348 //----------------------------------------------------------------------
349 NS_IMETHODIMP
350 nsComboboxControlFrame::DoneAddingChildren(bool aIsDone) { return NS_OK; }
352 NS_IMETHODIMP
353 nsComboboxControlFrame::AddOption(int32_t aIndex) {
354 if (aIndex <= mDisplayedIndex) {
355 ++mDisplayedIndex;
358 return NS_OK;
361 NS_IMETHODIMP
362 nsComboboxControlFrame::RemoveOption(int32_t aIndex) {
363 if (Select().Options()->Length()) {
364 if (aIndex < mDisplayedIndex) {
365 --mDisplayedIndex;
366 } else if (aIndex == mDisplayedIndex) {
367 mDisplayedIndex = 0; // IE6 compat
368 RedisplayText();
370 } else {
371 // If we removed the last option, we need to blank things out
372 mDisplayedIndex = -1;
373 RedisplayText();
375 return NS_OK;
378 NS_IMETHODIMP_(void)
379 nsComboboxControlFrame::OnSetSelectedIndex(int32_t aOldIndex,
380 int32_t aNewIndex) {
381 nsAutoScriptBlocker scriptBlocker;
382 mDisplayedIndex = aNewIndex;
383 RedisplayText();
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) {
395 return NS_OK;
398 return nsHTMLButtonControlFrame::HandleEvent(aPresContext, aEvent,
399 aEventStatus);
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,
431 false);
432 // Set tabindex="-1" so that the button is not tabbable
433 mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns,
434 false);
435 aElements.AppendElement(mButtonContent);
438 return NS_OK;
441 void nsComboboxControlFrame::AppendAnonymousContentTo(
442 nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
443 if (mDisplayLabel) {
444 aElements.AppendElement(mDisplayLabel);
447 if (mButtonContent) {
448 aElements.AppendElement(mButtonContent);
452 namespace mozilla {
454 class ComboboxLabelFrame final : public nsBlockFrame {
455 public:
456 NS_DECL_QUERYFRAME
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);
463 #endif
465 void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
466 const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final;
468 public:
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 //---------------------------------------------------------
518 NS_IMETHODIMP
519 nsComboboxControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) {
520 if (aSelected) {
521 nsAutoScriptBlocker blocker;
522 mDisplayedIndex = aIndex;
523 RedisplayText();
524 } else {
525 AutoWeakFrame weakFrame(this);
526 RedisplaySelectedText();
527 if (weakFrame.IsAlive()) {
528 FireValueChangeEvent(); // Fire after old option is unselected
531 return NS_OK;
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));