1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "nsComboboxControlFrame.h"
8
9 #include "gfxContext.h"
10 #include "gfxUtils.h"
11 #include "mozilla/gfx/2D.h"
12 #include "mozilla/gfx/PathHelpers.h"
13 #include "nsCOMPtr.h"
14 #include "nsDeviceContext.h"
15 #include "nsFocusManager.h"
16 #include "nsCheckboxRadioFrame.h"
17 #include "nsGkAtoms.h"
18 #include "nsCSSAnonBoxes.h"
19 #include "nsHTMLParts.h"
20 #include "nsIFormControl.h"
21 #include "nsILayoutHistoryState.h"
22 #include "nsNameSpaceManager.h"
23 #include "nsListControlFrame.h"
24 #include "nsPIDOMWindow.h"
25 #include "mozilla/PresState.h"
26 #include "nsView.h"
27 #include "nsViewManager.h"
28 #include "nsIContentInlines.h"
29 #include "nsIDOMEventListener.h"
30 #include "nsISelectControlFrame.h"
31 #include "nsContentUtils.h"
32 #include "mozilla/dom/Event.h"
33 #include "mozilla/dom/HTMLSelectElement.h"
34 #include "mozilla/dom/Document.h"
35 #include "nsIScrollableFrame.h"
36 #include "mozilla/ServoStyleSet.h"
37 #include "nsNodeInfoManager.h"
38 #include "nsContentCreatorFunctions.h"
39 #include "nsLayoutUtils.h"
40 #include "nsDisplayList.h"
41 #include "nsITheme.h"
42 #include "nsStyleConsts.h"
43 #include "nsTextFrameUtils.h"
44 #include "nsTextRunTransformations.h"
45 #include "HTMLSelectEventListener.h"
46 #include "mozilla/Likely.h"
47 #include <algorithm>
48 #include "nsTextNode.h"
49 #include "mozilla/AsyncEventDispatcher.h"
50 #include "mozilla/EventStates.h"
51 #include "mozilla/LookAndFeel.h"
52 #include "mozilla/MouseEvents.h"
53 #include "mozilla/PresShell.h"
54 #include "mozilla/PresShellInlines.h"
55 #include "mozilla/Unused.h"
56 #include "gfx2DGlue.h"
57 #include "mozilla/widget/nsAutoRollup.h"
58
59 using namespace mozilla;
60 using namespace mozilla::gfx;
61
62 NS_IMETHODIMP
Run()63 nsComboboxControlFrame::RedisplayTextEvent::Run() {
64 if (mControlFrame) mControlFrame->HandleRedisplayTextEvent();
65 return NS_OK;
66 }
67
68 // Drop down list event management.
69 // The combo box uses the following strategy for managing the drop-down list.
70 // If the combo box or its arrow button is clicked on the drop-down list is
71 // displayed If mouse exits the combo box with the drop-down list displayed the
72 // drop-down list is asked to capture events The drop-down list will capture all
73 // events including mouse down and up and will always return with
74 // ListWasSelected method call regardless of whether an item in the list was
75 // actually selected.
76 // The ListWasSelected code will turn off mouse-capture for the drop-down list.
77 // The drop-down list does not explicitly set capture when it is in the
78 // drop-down mode.
79
NS_NewComboboxControlFrame(PresShell * aPresShell,ComputedStyle * aStyle,nsFrameState aStateFlags)80 nsComboboxControlFrame* NS_NewComboboxControlFrame(PresShell* aPresShell,
81 ComputedStyle* aStyle,
82 nsFrameState aStateFlags) {
83 nsComboboxControlFrame* it = new (aPresShell)
84 nsComboboxControlFrame(aStyle, aPresShell->GetPresContext());
85
86 if (it) {
87 // set the state flags (if any are provided)
88 it->AddStateBits(aStateFlags);
89 }
90
91 return it;
92 }
93
94 NS_IMPL_FRAMEARENA_HELPERS(nsComboboxControlFrame)
95
96 //-----------------------------------------------------------
97 // Reflow Debugging Macros
98 // These let us "see" how many reflow counts are happening
99 //-----------------------------------------------------------
100 #ifdef DO_REFLOW_COUNTER
101
102 # define MAX_REFLOW_CNT 1024
103 static int32_t gTotalReqs = 0;
104 ;
105 static int32_t gTotalReflows = 0;
106 ;
107 static int32_t gReflowControlCntRQ[MAX_REFLOW_CNT];
108 static int32_t gReflowControlCnt[MAX_REFLOW_CNT];
109 static int32_t gReflowInx = -1;
110
111 # define REFLOW_COUNTER() \
112 if (mReflowId > -1) gReflowControlCnt[mReflowId]++;
113
114 # define REFLOW_COUNTER_REQUEST() \
115 if (mReflowId > -1) gReflowControlCntRQ[mReflowId]++;
116
117 # define REFLOW_COUNTER_DUMP(__desc) \
118 if (mReflowId > -1) { \
119 gTotalReqs += gReflowControlCntRQ[mReflowId]; \
120 gTotalReflows += gReflowControlCnt[mReflowId]; \
121 printf("** Id:%5d %s RF: %d RQ: %d %d/%d %5.2f\n", mReflowId, \
122 (__desc), gReflowControlCnt[mReflowId], \
123 gReflowControlCntRQ[mReflowId], gTotalReflows, gTotalReqs, \
124 float(gTotalReflows) / float(gTotalReqs) * 100.0f); \
125 }
126
127 # define REFLOW_COUNTER_INIT() \
128 if (gReflowInx < MAX_REFLOW_CNT) { \
129 gReflowInx++; \
130 mReflowId = gReflowInx; \
131 gReflowControlCnt[mReflowId] = 0; \
132 gReflowControlCntRQ[mReflowId] = 0; \
133 } else { \
134 mReflowId = -1; \
135 }
136
137 // reflow messages
138 # define REFLOW_DEBUG_MSG(_msg1) printf((_msg1))
139 # define REFLOW_DEBUG_MSG2(_msg1, _msg2) printf((_msg1), (_msg2))
140 # define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3) \
141 printf((_msg1), (_msg2), (_msg3))
142 # define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4) \
143 printf((_msg1), (_msg2), (_msg3), (_msg4))
144
145 #else //-------------
146
147 # define REFLOW_COUNTER_REQUEST()
148 # define REFLOW_COUNTER()
149 # define REFLOW_COUNTER_DUMP(__desc)
150 # define REFLOW_COUNTER_INIT()
151
152 # define REFLOW_DEBUG_MSG(_msg)
153 # define REFLOW_DEBUG_MSG2(_msg1, _msg2)
154 # define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3)
155 # define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4)
156
157 #endif
158
159 //------------------------------------------
160 // This is for being VERY noisy
161 //------------------------------------------
162 #ifdef DO_VERY_NOISY
163 # define REFLOW_NOISY_MSG(_msg1) printf((_msg1))
164 # define REFLOW_NOISY_MSG2(_msg1, _msg2) printf((_msg1), (_msg2))
165 # define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3) \
166 printf((_msg1), (_msg2), (_msg3))
167 # define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4) \
168 printf((_msg1), (_msg2), (_msg3), (_msg4))
169 #else
170 # define REFLOW_NOISY_MSG(_msg)
171 # define REFLOW_NOISY_MSG2(_msg1, _msg2)
172 # define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3)
173 # define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4)
174 #endif
175
176 //------------------------------------------
177 // Displays value in pixels or twips
178 //------------------------------------------
179 #ifdef DO_PIXELS
180 # define PX(__v) __v / 15
181 #else
182 # define PX(__v) __v
183 #endif
184
185 //------------------------------------------------------
186 //-- Done with macros
187 //------------------------------------------------------
188
nsComboboxControlFrame(ComputedStyle * aStyle,nsPresContext * aPresContext)189 nsComboboxControlFrame::nsComboboxControlFrame(ComputedStyle* aStyle,
190 nsPresContext* aPresContext)
191 : nsBlockFrame(aStyle, aPresContext, kClassID),
192 mDisplayFrame(nullptr),
193 mButtonFrame(nullptr),
194 mDisplayISize(0),
195 mMaxDisplayISize(0),
196 mRecentSelectedIndex(NS_SKIP_NOTIFY_INDEX),
197 mDisplayedIndex(-1),
198 mInRedisplayText(false),
199 mIsOpenInParentProcess(false){REFLOW_COUNTER_INIT()}
200
201 //--------------------------------------------------------------
~nsComboboxControlFrame()202 nsComboboxControlFrame::~nsComboboxControlFrame() {
203 REFLOW_COUNTER_DUMP("nsCCF");
204 }
205
206 //--------------------------------------------------------------
207
208 NS_QUERYFRAME_HEAD(nsComboboxControlFrame)
NS_QUERYFRAME_ENTRY(nsComboboxControlFrame)209 NS_QUERYFRAME_ENTRY(nsComboboxControlFrame)
210 NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
211 NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
212 NS_QUERYFRAME_ENTRY(nsISelectControlFrame)
213 NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame)
214
215 #ifdef ACCESSIBILITY
216 a11y::AccType nsComboboxControlFrame::AccessibleType() {
217 return a11y::eHTMLComboboxType;
218 }
219 #endif
220
SetFocus(bool aOn,bool aRepaint)221 void nsComboboxControlFrame::SetFocus(bool aOn, bool aRepaint) {
222 // This is needed on a temporary basis. It causes the focus
223 // rect to be drawn. This is much faster than ReResolvingStyle
224 // Bug 32920
225 InvalidateFrame();
226 }
227
GetCSSTransformTranslation()228 nsPoint nsComboboxControlFrame::GetCSSTransformTranslation() {
229 nsIFrame* frame = this;
230 bool is3DTransform = false;
231 Matrix transform;
232 while (frame) {
233 nsIFrame* parent;
234 Matrix4x4Flagged ctm = frame->GetTransformMatrix(
235 ViewportType::Layout, RelativeTo{nullptr}, &parent);
236 Matrix matrix;
237 if (ctm.Is2D(&matrix)) {
238 transform = transform * matrix;
239 } else {
240 is3DTransform = true;
241 break;
242 }
243 frame = parent;
244 }
245 nsPoint translation;
246 if (!is3DTransform && !transform.HasNonTranslation()) {
247 nsPresContext* pc = PresContext();
248 // To get the translation introduced only by transforms we subtract the
249 // regular non-transform translation.
250 nsRootPresContext* rootPC = pc->GetRootPresContext();
251 if (rootPC) {
252 int32_t apd = pc->AppUnitsPerDevPixel();
253 translation.x = NSFloatPixelsToAppUnits(transform._31, apd);
254 translation.y = NSFloatPixelsToAppUnits(transform._32, apd);
255 translation -= GetOffsetToCrossDoc(rootPC->PresShell()->GetRootFrame());
256 }
257 }
258 return translation;
259 }
260
261 //----------------------------------------------------------
262 //
263 //----------------------------------------------------------
264 #ifdef DO_REFLOW_DEBUG
265 static int myCounter = 0;
266
printSize(char * aDesc,nscoord aSize)267 static void printSize(char* aDesc, nscoord aSize) {
268 printf(" %s: ", aDesc);
269 if (aSize == NS_UNCONSTRAINEDSIZE) {
270 printf("UC");
271 } else {
272 printf("%d", PX(aSize));
273 }
274 }
275 #endif
276
277 //-------------------------------------------------------------------
278 //-- Main Reflow for the Combobox
279 //-------------------------------------------------------------------
280
HasDropDownButton() const281 bool nsComboboxControlFrame::HasDropDownButton() const {
282 const nsStyleDisplay* disp = StyleDisplay();
283 // FIXME(emilio): Blink also shows this for menulist-button and such... Seems
284 // more similar to our mac / linux implementation.
285 return disp->EffectiveAppearance() == StyleAppearance::Menulist &&
286 (!IsThemed(disp) ||
287 PresContext()->Theme()->ThemeNeedsComboboxDropmarker());
288 }
289
DropDownButtonISize()290 nscoord nsComboboxControlFrame::DropDownButtonISize() {
291 if (!HasDropDownButton()) {
292 return 0;
293 }
294
295 LayoutDeviceIntSize dropdownButtonSize;
296 bool canOverride = true;
297 nsPresContext* presContext = PresContext();
298 presContext->Theme()->GetMinimumWidgetSize(
299 presContext, this, StyleAppearance::MozMenulistArrowButton,
300 &dropdownButtonSize, &canOverride);
301
302 return presContext->DevPixelsToAppUnits(dropdownButtonSize.width);
303 }
304
CharCountOfLargestOptionForInflation() const305 int32_t nsComboboxControlFrame::CharCountOfLargestOptionForInflation() const {
306 uint32_t maxLength = 0;
307 nsAutoString label;
308 for (auto i : IntegerRange(Select().Options()->Length())) {
309 GetOptionText(i, label);
310 maxLength = std::max(
311 maxLength,
312 nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression(
313 label, StyleText()));
314 }
315 if (MOZ_UNLIKELY(maxLength > uint32_t(INT32_MAX))) {
316 return INT32_MAX;
317 }
318 return int32_t(maxLength);
319 }
320
GetIntrinsicISize(gfxContext * aRenderingContext,IntrinsicISizeType aType)321 nscoord nsComboboxControlFrame::GetIntrinsicISize(gfxContext* aRenderingContext,
322 IntrinsicISizeType aType) {
323 nscoord displayISize = mDisplayFrame->IntrinsicISizeOffsets().padding;
324
325 if (!StyleDisplay()->IsContainSize() && !StyleContent()->mContent.IsNone()) {
326 // Compute the width of each option's (potentially text-transformed) text,
327 // and use the widest one as part of our intrinsic size.
328 nscoord maxOptionSize = 0;
329 nsAutoString label;
330 nsAutoString transformedLabel;
331 RefPtr<nsFontMetrics> fm =
332 nsLayoutUtils::GetInflatedFontMetricsForFrame(this);
333 auto textTransform = StyleText()->mTextTransform.IsNone()
334 ? Nothing()
335 : Some(StyleText()->mTextTransform);
336 nsAtom* language = StyleFont()->mLanguage;
337 AutoTArray<bool, 50> charsToMergeArray;
338 AutoTArray<bool, 50> deletedCharsArray;
339 for (auto i : IntegerRange(Select().Options()->Length())) {
340 GetOptionText(i, label);
341 const nsAutoString* stringToUse = &label;
342 if (textTransform) {
343 transformedLabel.Truncate();
344 charsToMergeArray.SetLengthAndRetainStorage(0);
345 deletedCharsArray.SetLengthAndRetainStorage(0);
346 nsCaseTransformTextRunFactory::TransformString(
347 label, transformedLabel, textTransform,
348 /* aCaseTransformsOnly = */ false, language, charsToMergeArray,
349 deletedCharsArray);
350 stringToUse = &transformedLabel;
351 }
352 maxOptionSize = std::max(
353 maxOptionSize, nsLayoutUtils::AppUnitWidthOfStringBidi(
354 *stringToUse, this, *fm, *aRenderingContext));
355 }
356
357 displayISize += maxOptionSize;
358 }
359
360 // Add room for the dropmarker button (if there is one) and scrollbar on the
361 // popup.
362 displayISize += DropDownButtonISize();
363 nsPresContext* pc = PresContext();
364 if (!pc->UseOverlayScrollbars()) {
365 displayISize += nsIScrollableFrame::GetNondisappearingScrollbarWidth(
366 pc, GetWritingMode());
367 }
368
369 return displayISize;
370 }
371
GetMinISize(gfxContext * aRenderingContext)372 nscoord nsComboboxControlFrame::GetMinISize(gfxContext* aRenderingContext) {
373 nscoord minISize;
374 DISPLAY_MIN_INLINE_SIZE(this, minISize);
375 minISize = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::MinISize);
376 return minISize;
377 }
378
GetPrefISize(gfxContext * aRenderingContext)379 nscoord nsComboboxControlFrame::GetPrefISize(gfxContext* aRenderingContext) {
380 nscoord prefISize;
381 DISPLAY_PREF_INLINE_SIZE(this, prefISize);
382 prefISize =
383 GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::PrefISize);
384 return prefISize;
385 }
386
Select() const387 dom::HTMLSelectElement& nsComboboxControlFrame::Select() const {
388 return *static_cast<dom::HTMLSelectElement*>(GetContent());
389 }
390
GetOptionText(uint32_t aIndex,nsAString & aText) const391 void nsComboboxControlFrame::GetOptionText(uint32_t aIndex,
392 nsAString& aText) const {
393 aText.Truncate();
394 if (Element* el = Select().Options()->GetElementAt(aIndex)) {
395 static_cast<dom::HTMLOptionElement*>(el)->GetRenderedLabel(aText);
396 }
397 }
398
Reflow(nsPresContext * aPresContext,ReflowOutput & aDesiredSize,const ReflowInput & aReflowInput,nsReflowStatus & aStatus)399 void nsComboboxControlFrame::Reflow(nsPresContext* aPresContext,
400 ReflowOutput& aDesiredSize,
401 const ReflowInput& aReflowInput,
402 nsReflowStatus& aStatus) {
403 MarkInReflow();
404 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
405 // Constraints we try to satisfy:
406
407 // 1) Default inline size of button is the vertical scrollbar size
408 // 2) If the inline size of button is bigger than our inline size, set
409 // inline size of button to 0.
410 // 3) Default block size of button is block size of display area
411 // 4) Inline size of display area is whatever is left over from our
412 // inline size after allocating inline size for the button.
413
414 if (!mDisplayFrame || !mButtonFrame) {
415 NS_ERROR("Why did the frame constructor allow this to happen? Fix it!!");
416 return;
417 }
418
419 // Make sure the displayed text is the same as the selected option,
420 // bug 297389.
421 mDisplayedIndex = Select().SelectedIndex();
422
423 // In dropped down mode the "selected index" is the hovered menu item,
424 // we want the last selected item which is |mDisplayedIndex| in this case.
425 RedisplayText();
426
427 WritingMode wm = aReflowInput.GetWritingMode();
428
429 // Check if the theme specifies a minimum size for the dropdown button
430 // first.
431 const nscoord buttonISize = DropDownButtonISize();
432 const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(wm);
433 const auto padding = aReflowInput.ComputedLogicalPadding(wm);
434 const auto border = borderPadding - padding;
435
436 mDisplayISize = aReflowInput.ComputedISize() - buttonISize;
437 mMaxDisplayISize = mDisplayISize + padding.IEnd(wm);
438
439 nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus);
440
441 // The button should occupy the same space as a scrollbar, and its position
442 // starts from the border edge.
443 LogicalRect buttonRect(wm);
444 buttonRect.IStart(wm) = borderPadding.IStart(wm) + mMaxDisplayISize;
445 buttonRect.BStart(wm) = border.BStart(wm);
446
447 buttonRect.ISize(wm) = buttonISize;
448 buttonRect.BSize(wm) = mDisplayFrame->BSize(wm) + padding.BStartEnd(wm);
449
450 const nsSize containerSize = aDesiredSize.PhysicalSize();
451 mButtonFrame->SetRect(buttonRect, containerSize);
452
453 if (!aStatus.IsInlineBreakBefore() && !aStatus.IsFullyComplete()) {
454 // This frame didn't fit inside a fragmentation container. Splitting
455 // a nsComboboxControlFrame makes no sense, so we override the status here.
456 aStatus.Reset();
457 }
458 }
459
Init(nsIContent * aContent,nsContainerFrame * aParent,nsIFrame * aPrevInFlow)460 void nsComboboxControlFrame::Init(nsIContent* aContent,
461 nsContainerFrame* aParent,
462 nsIFrame* aPrevInFlow) {
463 nsBlockFrame::Init(aContent, aParent, aPrevInFlow);
464
465 mEventListener = new HTMLSelectEventListener(
466 Select(), HTMLSelectEventListener::SelectType::Combobox);
467 }
468
469 #ifdef DEBUG_FRAME_DUMP
GetFrameName(nsAString & aResult) const470 nsresult nsComboboxControlFrame::GetFrameName(nsAString& aResult) const {
471 return MakeFrameName(u"ComboboxControl"_ns, aResult);
472 }
473 #endif
474
475 ///////////////////////////////////////////////////////////////
476
RedisplaySelectedText()477 nsresult nsComboboxControlFrame::RedisplaySelectedText() {
478 nsAutoScriptBlocker scriptBlocker;
479 mDisplayedIndex = Select().SelectedIndex();
480 return RedisplayText();
481 }
482
RedisplayText()483 nsresult nsComboboxControlFrame::RedisplayText() {
484 nsString previewValue;
485 nsString previousText(mDisplayedOptionTextOrPreview);
486
487 Select().GetPreviewValue(previewValue);
488 // Get the text to display
489 if (!previewValue.IsEmpty()) {
490 mDisplayedOptionTextOrPreview = previewValue;
491 } else if (mDisplayedIndex != -1 && !StyleContent()->mContent.IsNone()) {
492 GetOptionText(mDisplayedIndex, mDisplayedOptionTextOrPreview);
493 } else {
494 mDisplayedOptionTextOrPreview.Truncate();
495 }
496
497 REFLOW_DEBUG_MSG2(
498 "RedisplayText \"%s\"\n",
499 NS_LossyConvertUTF16toASCII(mDisplayedOptionTextOrPreview).get());
500
501 // Send reflow command because the new text maybe larger
502 nsresult rv = NS_OK;
503 if (mDisplayContent && !previousText.Equals(mDisplayedOptionTextOrPreview)) {
504 // Don't call ActuallyDisplayText(true) directly here since that
505 // could cause recursive frame construction. See bug 283117 and the comment
506 // in HandleRedisplayTextEvent() below.
507
508 // Revoke outstanding events to avoid out-of-order events which could mean
509 // displaying the wrong text.
510 mRedisplayTextEvent.Revoke();
511
512 NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(),
513 "If we happen to run our redisplay event now, we might kill "
514 "ourselves!");
515
516 mRedisplayTextEvent = new RedisplayTextEvent(this);
517 nsContentUtils::AddScriptRunner(mRedisplayTextEvent.get());
518 }
519 return rv;
520 }
521
HandleRedisplayTextEvent()522 void nsComboboxControlFrame::HandleRedisplayTextEvent() {
523 // First, make sure that the content model is up to date and we've
524 // constructed the frames for all our content in the right places.
525 // Otherwise they'll end up under the wrong insertion frame when we
526 // ActuallyDisplayText, since that flushes out the content sink by
527 // calling SetText on a DOM node with aNotify set to true. See bug
528 // 289730.
529 AutoWeakFrame weakThis(this);
530 PresContext()->Document()->FlushPendingNotifications(
531 FlushType::ContentAndNotify);
532 if (!weakThis.IsAlive()) return;
533
534 // Redirect frame insertions during this method (see
535 // GetContentInsertionFrame()) so that any reframing that the frame
536 // constructor forces upon us is inserted into the correct parent
537 // (mDisplayFrame). See bug 282607.
538 MOZ_ASSERT(!mInRedisplayText, "Nested RedisplayText");
539 mInRedisplayText = true;
540 mRedisplayTextEvent.Forget();
541
542 ActuallyDisplayText(true);
543 if (!weakThis.IsAlive()) {
544 return;
545 }
546
547 // XXXbz This should perhaps be eResize. Check.
548 PresShell()->FrameNeedsReflow(mDisplayFrame, IntrinsicDirty::StyleChange,
549 NS_FRAME_IS_DIRTY);
550
551 mInRedisplayText = false;
552 }
553
ActuallyDisplayText(bool aNotify)554 void nsComboboxControlFrame::ActuallyDisplayText(bool aNotify) {
555 RefPtr<nsTextNode> displayContent = mDisplayContent;
556 if (mDisplayedOptionTextOrPreview.IsEmpty()) {
557 // Have to use a space character of some sort for line-block-size
558 // calculations to be right. Also, the space character must be zero-width
559 // in order for the the inline-size calculations to be consistent between
560 // size-contained comboboxes vs. empty comboboxes.
561 //
562 // XXXdholbert Does this space need to be "non-breaking"? I'm not sure
563 // if it matters, but we previously had a comment here (added in 2002)
564 // saying "Have to use a non-breaking space for line-height calculations
565 // to be right". So I'll stick with a non-breaking space for now...
566 static const char16_t space = 0xFEFF;
567 displayContent->SetText(&space, 1, aNotify);
568 } else {
569 displayContent->SetText(mDisplayedOptionTextOrPreview, aNotify);
570 }
571 }
572
GetIndexOfDisplayArea()573 int32_t nsComboboxControlFrame::GetIndexOfDisplayArea() {
574 return mDisplayedIndex;
575 }
576
IsDroppedDown() const577 bool nsComboboxControlFrame::IsDroppedDown() const {
578 return Select().OpenInParentProcess();
579 }
580
581 //----------------------------------------------------------------------
582 // nsISelectControlFrame
583 //----------------------------------------------------------------------
584 NS_IMETHODIMP
DoneAddingChildren(bool aIsDone)585 nsComboboxControlFrame::DoneAddingChildren(bool aIsDone) { return NS_OK; }
586
587 NS_IMETHODIMP
AddOption(int32_t aIndex)588 nsComboboxControlFrame::AddOption(int32_t aIndex) {
589 if (aIndex <= mDisplayedIndex) {
590 ++mDisplayedIndex;
591 }
592
593 return NS_OK;
594 }
595
596 NS_IMETHODIMP
RemoveOption(int32_t aIndex)597 nsComboboxControlFrame::RemoveOption(int32_t aIndex) {
598 if (Select().Options()->Length()) {
599 if (aIndex < mDisplayedIndex) {
600 --mDisplayedIndex;
601 } else if (aIndex == mDisplayedIndex) {
602 mDisplayedIndex = 0; // IE6 compat
603 RedisplayText();
604 }
605 } else {
606 // If we removed the last option, we need to blank things out
607 mDisplayedIndex = -1;
608 RedisplayText();
609 }
610 return NS_OK;
611 }
612
NS_IMETHODIMP_(void)613 NS_IMETHODIMP_(void)
614 nsComboboxControlFrame::OnSetSelectedIndex(int32_t aOldIndex,
615 int32_t aNewIndex) {
616 nsAutoScriptBlocker scriptBlocker;
617 mDisplayedIndex = aNewIndex;
618 RedisplayText();
619 }
620
621 // End nsISelectControlFrame
622 //----------------------------------------------------------------------
623
HandleEvent(nsPresContext * aPresContext,WidgetGUIEvent * aEvent,nsEventStatus * aEventStatus)624 nsresult nsComboboxControlFrame::HandleEvent(nsPresContext* aPresContext,
625 WidgetGUIEvent* aEvent,
626 nsEventStatus* aEventStatus) {
627 NS_ENSURE_ARG_POINTER(aEventStatus);
628
629 if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
630 return NS_OK;
631 }
632
633 EventStates eventStates = mContent->AsElement()->State();
634 if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
635 return NS_OK;
636 }
637
638 // If we have style that affects how we are selected, feed event down to
639 // nsIFrame::HandleEvent so that selection takes place when appropriate.
640 if (IsContentDisabled()) {
641 return nsBlockFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
642 }
643 return NS_OK;
644 }
645
GetContentInsertionFrame()646 nsContainerFrame* nsComboboxControlFrame::GetContentInsertionFrame() {
647 return mInRedisplayText ? mDisplayFrame : nullptr;
648 }
649
AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox> & aResult)650 void nsComboboxControlFrame::AppendDirectlyOwnedAnonBoxes(
651 nsTArray<OwnedAnonBox>& aResult) {
652 aResult.AppendElement(OwnedAnonBox(mDisplayFrame));
653 }
654
CreateAnonymousContent(nsTArray<ContentInfo> & aElements)655 nsresult nsComboboxControlFrame::CreateAnonymousContent(
656 nsTArray<ContentInfo>& aElements) {
657 // The frames used to display the combo box and the button used to popup the
658 // dropdown list are created through anonymous content. The dropdown list is
659 // not created through anonymous content because its frame is initialized
660 // specifically for the drop-down case and it is placed a special list
661 // referenced through NS_COMBO_FRAME_POPUP_LIST_INDEX to keep separate from
662 // the layout of the display and button.
663 //
664 // Note: The value attribute of the display content is set when an item is
665 // selected in the dropdown list. If the content specified below does not
666 // honor the value attribute than nothing will be displayed.
667
668 // For now the content that is created corresponds to two input buttons. It
669 // would be better to create the tag as something other than input, but then
670 // there isn't any way to create a button frame since it isn't possible to set
671 // the display type in CSS2 to create a button frame.
672
673 // create content used for display
674 // nsAtom* tag = NS_Atomize("mozcombodisplay");
675
676 // Add a child text content node for the label
677
678 nsNodeInfoManager* nimgr = mContent->NodeInfo()->NodeInfoManager();
679
680 mDisplayContent = new (nimgr) nsTextNode(nimgr);
681
682 // set the value of the text node
683 mDisplayedIndex = Select().SelectedIndex();
684 if (mDisplayedIndex != -1) {
685 GetOptionText(mDisplayedIndex, mDisplayedOptionTextOrPreview);
686 }
687 ActuallyDisplayText(false);
688
689 // XXX(Bug 1631371) Check if this should use a fallible operation as it
690 // pretended earlier.
691 aElements.AppendElement(mDisplayContent);
692
693 mButtonContent = mContent->OwnerDoc()->CreateHTMLElement(nsGkAtoms::button);
694 if (!mButtonContent) return NS_ERROR_OUT_OF_MEMORY;
695
696 // make someone to listen to the button.
697 mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::type, u"button"_ns,
698 false);
699 // Set tabindex="-1" so that the button is not tabbable
700 mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns,
701 false);
702
703 WritingMode wm = GetWritingMode();
704 if (wm.IsVertical()) {
705 mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::orientation,
706 wm.IsVerticalRL() ? u"left"_ns : u"right"_ns,
707 false);
708 }
709
710 // XXX(Bug 1631371) Check if this should use a fallible operation as it
711 // pretended earlier.
712 aElements.AppendElement(mButtonContent);
713
714 return NS_OK;
715 }
716
AppendAnonymousContentTo(nsTArray<nsIContent * > & aElements,uint32_t aFilter)717 void nsComboboxControlFrame::AppendAnonymousContentTo(
718 nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
719 if (mDisplayContent) {
720 aElements.AppendElement(mDisplayContent);
721 }
722
723 if (mButtonContent) {
724 aElements.AppendElement(mButtonContent);
725 }
726 }
727
GetDisplayNode() const728 nsIContent* nsComboboxControlFrame::GetDisplayNode() const {
729 return mDisplayContent;
730 }
731
732 // XXXbz this is a for-now hack. Now that display:inline-block works,
733 // need to revisit this.
734 class nsComboboxDisplayFrame final : public nsBlockFrame {
735 public:
736 NS_DECL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame)
737
nsComboboxDisplayFrame(ComputedStyle * aStyle,nsComboboxControlFrame * aComboBox)738 nsComboboxDisplayFrame(ComputedStyle* aStyle,
739 nsComboboxControlFrame* aComboBox)
740 : nsBlockFrame(aStyle, aComboBox->PresContext(), kClassID),
741 mComboBox(aComboBox) {}
742
743 #ifdef DEBUG_FRAME_DUMP
GetFrameName(nsAString & aResult) const744 nsresult GetFrameName(nsAString& aResult) const final {
745 return MakeFrameName(u"ComboboxDisplay"_ns, aResult);
746 }
747 #endif
748
IsFrameOfType(uint32_t aFlags) const749 bool IsFrameOfType(uint32_t aFlags) const final {
750 return nsBlockFrame::IsFrameOfType(aFlags &
751 ~(nsIFrame::eReplacedContainsBlock));
752 }
753
754 void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
755 const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final;
756
757 void BuildDisplayList(nsDisplayListBuilder* aBuilder,
758 const nsDisplayListSet& aLists) final;
759
760 protected:
761 nsComboboxControlFrame* mComboBox;
762 };
763
NS_IMPL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame)764 NS_IMPL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame)
765
766 void nsComboboxDisplayFrame::Reflow(nsPresContext* aPresContext,
767 ReflowOutput& aDesiredSize,
768 const ReflowInput& aReflowInput,
769 nsReflowStatus& aStatus) {
770 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
771 MOZ_ASSERT(aReflowInput.mParentReflowInput &&
772 aReflowInput.mParentReflowInput->mFrame == mComboBox,
773 "Combobox's frame tree is wrong!");
774
775 ReflowInput state(aReflowInput);
776 if (state.ComputedBSize() == NS_UNCONSTRAINEDSIZE) {
777 state.SetLineHeight(state.mParentReflowInput->GetLineHeight());
778 }
779 const WritingMode wm = aReflowInput.GetWritingMode();
780 const LogicalMargin bp = state.ComputedLogicalBorderPadding(wm);
781 MOZ_ASSERT(bp.BStartEnd(wm) == 0,
782 "We shouldn't have border and padding in the block axis in UA!");
783 nscoord inlineBp = bp.IStartEnd(wm);
784 nscoord computedISize = mComboBox->mDisplayISize - inlineBp;
785
786 // Other UAs ignore padding in some (but not all) platforms for (themed only)
787 // comboboxes. Instead of doing that, we prevent that padding if present from
788 // clipping the display text, by enforcing the display text minimum size in
789 // that situation.
790 const bool shouldHonorMinISize =
791 mComboBox->StyleDisplay()->EffectiveAppearance() ==
792 StyleAppearance::Menulist;
793 if (shouldHonorMinISize) {
794 computedISize = std::max(state.ComputedMinISize(), computedISize);
795 // Don't let this size go over mMaxDisplayISize, since that'd be
796 // observable via clientWidth / scrollWidth.
797 computedISize =
798 std::min(computedISize, mComboBox->mMaxDisplayISize - inlineBp);
799 }
800
801 state.SetComputedISize(std::max(0, computedISize));
802 nsBlockFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
803 aStatus.Reset(); // this type of frame can't be split
804 }
805
BuildDisplayList(nsDisplayListBuilder * aBuilder,const nsDisplayListSet & aLists)806 void nsComboboxDisplayFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
807 const nsDisplayListSet& aLists) {
808 nsDisplayListCollection set(aBuilder);
809 nsBlockFrame::BuildDisplayList(aBuilder, set);
810
811 // remove background items if parent frame is themed
812 if (mComboBox->IsThemed()) {
813 set.BorderBackground()->DeleteAll(aBuilder);
814 }
815
816 set.MoveTo(aLists);
817 }
818
CreateFrameForDisplayNode()819 nsIFrame* nsComboboxControlFrame::CreateFrameForDisplayNode() {
820 MOZ_ASSERT(mDisplayContent);
821
822 // Get PresShell
823 mozilla::PresShell* ps = PresShell();
824 ServoStyleSet* styleSet = ps->StyleSet();
825
826 // create the ComputedStyle for the anonymous block frame and text frame
827 RefPtr<ComputedStyle> computedStyle =
828 styleSet->ResolveInheritingAnonymousBoxStyle(
829 PseudoStyleType::mozDisplayComboboxControlFrame, mComputedStyle);
830
831 RefPtr<ComputedStyle> textComputedStyle =
832 styleSet->ResolveStyleForText(mDisplayContent, mComputedStyle);
833
834 // Start by creating our anonymous block frame
835 mDisplayFrame = new (ps) nsComboboxDisplayFrame(computedStyle, this);
836 mDisplayFrame->Init(mContent, this, nullptr);
837
838 // Create a text frame and put it inside the block frame
839 nsIFrame* textFrame = NS_NewTextFrame(ps, textComputedStyle);
840
841 // initialize the text frame
842 textFrame->Init(mDisplayContent, mDisplayFrame, nullptr);
843 mDisplayContent->SetPrimaryFrame(textFrame);
844
845 nsFrameList textList(textFrame, textFrame);
846 mDisplayFrame->SetInitialChildList(kPrincipalList, textList);
847 return mDisplayFrame;
848 }
849
DestroyFrom(nsIFrame * aDestructRoot,PostDestroyData & aPostDestroyData)850 void nsComboboxControlFrame::DestroyFrom(nsIFrame* aDestructRoot,
851 PostDestroyData& aPostDestroyData) {
852 // Revoke any pending RedisplayTextEvent
853 mRedisplayTextEvent.Revoke();
854
855 mEventListener->Detach();
856
857 // Cleanup frames in popup child list
858 mPopupFrames.DestroyFramesFrom(aDestructRoot, aPostDestroyData);
859 aPostDestroyData.AddAnonymousContent(mDisplayContent.forget());
860 aPostDestroyData.AddAnonymousContent(mButtonContent.forget());
861 nsBlockFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
862 }
863
GetChildList(ChildListID aListID) const864 const nsFrameList& nsComboboxControlFrame::GetChildList(
865 ChildListID aListID) const {
866 if (kSelectPopupList == aListID) {
867 return mPopupFrames;
868 }
869 return nsBlockFrame::GetChildList(aListID);
870 }
871
GetChildLists(nsTArray<ChildList> * aLists) const872 void nsComboboxControlFrame::GetChildLists(nsTArray<ChildList>* aLists) const {
873 nsBlockFrame::GetChildLists(aLists);
874 mPopupFrames.AppendIfNonempty(aLists, kSelectPopupList);
875 }
876
SetInitialChildList(ChildListID aListID,nsFrameList & aChildList)877 void nsComboboxControlFrame::SetInitialChildList(ChildListID aListID,
878 nsFrameList& aChildList) {
879 #ifdef DEBUG
880 for (nsIFrame* f : aChildList) {
881 MOZ_ASSERT(f->GetParent() == this, "Unexpected parent");
882 }
883 #endif
884 if (kSelectPopupList == aListID) {
885 mPopupFrames.SetFrames(aChildList);
886 } else {
887 for (nsFrameList::Enumerator e(aChildList); !e.AtEnd(); e.Next()) {
888 nsCOMPtr<nsIFormControl> formControl =
889 do_QueryInterface(e.get()->GetContent());
890 if (formControl &&
891 formControl->ControlType() == FormControlType::ButtonButton) {
892 mButtonFrame = e.get();
893 break;
894 }
895 }
896 NS_ASSERTION(mButtonFrame, "missing button frame in initial child list");
897 nsBlockFrame::SetInitialChildList(aListID, aChildList);
898 }
899 }
900
901 namespace mozilla {
902
903 class nsDisplayComboboxFocus : public nsPaintedDisplayItem {
904 public:
nsDisplayComboboxFocus(nsDisplayListBuilder * aBuilder,nsComboboxControlFrame * aFrame)905 nsDisplayComboboxFocus(nsDisplayListBuilder* aBuilder,
906 nsComboboxControlFrame* aFrame)
907 : nsPaintedDisplayItem(aBuilder, aFrame) {
908 MOZ_COUNT_CTOR(nsDisplayComboboxFocus);
909 }
910 MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayComboboxFocus)
911
912 void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
913 NS_DISPLAY_DECL_NAME("ComboboxFocus", TYPE_COMBOBOX_FOCUS)
914 };
915
Paint(nsDisplayListBuilder * aBuilder,gfxContext * aCtx)916 void nsDisplayComboboxFocus::Paint(nsDisplayListBuilder* aBuilder,
917 gfxContext* aCtx) {
918 static_cast<nsComboboxControlFrame*>(mFrame)->PaintFocus(
919 *aCtx->GetDrawTarget(), ToReferenceFrame());
920 }
921
922 } // namespace mozilla
923
BuildDisplayList(nsDisplayListBuilder * aBuilder,const nsDisplayListSet & aLists)924 void nsComboboxControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
925 const nsDisplayListSet& aLists) {
926 if (aBuilder->IsForEventDelivery()) {
927 // Don't allow children to receive events.
928 // REVIEW: following old GetFrameForPoint
929 DisplayBorderBackgroundOutline(aBuilder, aLists);
930 } else {
931 // REVIEW: Our in-flow child frames are inline-level so they will paint in
932 // our content list, so we don't need to mess with layers.
933 nsBlockFrame::BuildDisplayList(aBuilder, aLists);
934 }
935
936 // draw a focus indicator only when focus rings should be drawn
937 if (mContent->AsElement()->State().HasState(NS_EVENT_STATE_FOCUSRING)) {
938 nsPresContext* pc = PresContext();
939 const nsStyleDisplay* disp = StyleDisplay();
940 if (IsThemed(disp) &&
941 pc->Theme()->ThemeWantsButtonInnerFocusRing(
942 this, disp->EffectiveAppearance()) &&
943 mDisplayFrame && IsVisibleForPainting()) {
944 aLists.Content()->AppendNewToTop<nsDisplayComboboxFocus>(aBuilder, this);
945 }
946 }
947
948 DisplaySelectionOverlay(aBuilder, aLists.Content());
949 }
950
PaintFocus(DrawTarget & aDrawTarget,nsPoint aPt)951 void nsComboboxControlFrame::PaintFocus(DrawTarget& aDrawTarget, nsPoint aPt) {
952 /* Do we need to do anything? */
953 EventStates eventStates = mContent->AsElement()->State();
954 if (eventStates.HasState(NS_EVENT_STATE_DISABLED) ||
955 !eventStates.HasState(NS_EVENT_STATE_FOCUS)) {
956 return;
957 }
958
959 int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
960
961 nsRect clipRect = mDisplayFrame->GetRect() + aPt;
962 aDrawTarget.PushClipRect(
963 NSRectToSnappedRect(clipRect, appUnitsPerDevPixel, aDrawTarget));
964
965 StrokeOptions strokeOptions;
966 nsLayoutUtils::InitDashPattern(strokeOptions, StyleBorderStyle::Dotted);
967 ColorPattern color(ToDeviceColor(StyleText()->mColor));
968 nscoord onePixel = nsPresContext::CSSPixelsToAppUnits(1);
969 clipRect.width -= onePixel;
970 clipRect.height -= onePixel;
971 Rect r = ToRect(nsLayoutUtils::RectToGfxRect(clipRect, appUnitsPerDevPixel));
972 StrokeSnappedEdgesOfRect(r, aDrawTarget, color, strokeOptions);
973
974 aDrawTarget.PopClip();
975 }
976
977 //---------------------------------------------------------
978 // gets the content (an option) by index and then set it as
979 // being selected or not selected
980 //---------------------------------------------------------
981 NS_IMETHODIMP
OnOptionSelected(int32_t aIndex,bool aSelected)982 nsComboboxControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) {
983 if (aSelected) {
984 nsAutoScriptBlocker blocker;
985 mDisplayedIndex = aIndex;
986 RedisplayText();
987 } else {
988 AutoWeakFrame weakFrame(this);
989 RedisplaySelectedText();
990 if (weakFrame.IsAlive()) {
991 FireValueChangeEvent(); // Fire after old option is unselected
992 }
993 }
994 return NS_OK;
995 }
996
FireValueChangeEvent()997 void nsComboboxControlFrame::FireValueChangeEvent() {
998 // Fire ValueChange event to indicate data value of combo box has changed
999 nsContentUtils::AddScriptRunner(new AsyncEventDispatcher(
1000 mContent, u"ValueChange"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo));
1001 }
1002