nsComboboxControlFrame.cpp (17544B)
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 <algorithm> 10 11 #include "HTMLSelectEventListener.h" 12 #include "gfxContext.h" 13 #include "gfxUtils.h" 14 #include "mozilla/AsyncEventDispatcher.h" 15 #include "mozilla/Likely.h" 16 #include "mozilla/PresShell.h" 17 #include "mozilla/PresShellInlines.h" 18 #include "mozilla/dom/Document.h" 19 #include "mozilla/dom/HTMLSelectElement.h" 20 #include "nsCOMPtr.h" 21 #include "nsContentUtils.h" 22 #include "nsGkAtoms.h" 23 #include "nsISelectControlFrame.h" 24 #include "nsITheme.h" 25 #include "nsLayoutUtils.h" 26 #include "nsStyleConsts.h" 27 #include "nsTextFrameUtils.h" 28 #include "nsTextNode.h" 29 #include "nsTextRunTransformations.h" 30 31 using namespace mozilla; 32 using namespace mozilla::gfx; 33 34 NS_IMETHODIMP 35 nsComboboxControlFrame::RedisplayTextEvent::Run() { 36 if (mControlFrame) { 37 mControlFrame->HandleRedisplayTextEvent(); 38 } 39 return NS_OK; 40 } 41 42 // Drop down list event management. 43 // The combo box uses the following strategy for managing the drop-down list. 44 // If the combo box or its arrow button is clicked on the drop-down list is 45 // displayed If mouse exits the combo box with the drop-down list displayed the 46 // drop-down list is asked to capture events The drop-down list will capture all 47 // events including mouse down and up and will always return with 48 // ListWasSelected method call regardless of whether an item in the list was 49 // actually selected. 50 // The ListWasSelected code will turn off mouse-capture for the drop-down list. 51 // The drop-down list does not explicitly set capture when it is in the 52 // drop-down mode. 53 54 nsComboboxControlFrame* NS_NewComboboxControlFrame(PresShell* aPresShell, 55 ComputedStyle* aStyle) { 56 return new (aPresShell) 57 nsComboboxControlFrame(aStyle, aPresShell->GetPresContext()); 58 } 59 60 NS_IMPL_FRAMEARENA_HELPERS(nsComboboxControlFrame) 61 62 nsComboboxControlFrame::nsComboboxControlFrame(ComputedStyle* aStyle, 63 nsPresContext* aPresContext) 64 : ButtonControlFrame(aStyle, aPresContext, kClassID) {} 65 66 nsComboboxControlFrame::~nsComboboxControlFrame() = default; 67 68 NS_QUERYFRAME_HEAD(nsComboboxControlFrame) 69 NS_QUERYFRAME_ENTRY(nsComboboxControlFrame) 70 NS_QUERYFRAME_ENTRY(nsISelectControlFrame) 71 NS_QUERYFRAME_TAIL_INHERITING(ButtonControlFrame) 72 73 #ifdef ACCESSIBILITY 74 a11y::AccType nsComboboxControlFrame::AccessibleType() { 75 return a11y::eHTMLComboboxType; 76 } 77 #endif 78 79 bool nsComboboxControlFrame::HasDropDownButton() const { 80 const nsStyleDisplay* disp = StyleDisplay(); 81 switch (disp->EffectiveAppearance()) { 82 case StyleAppearance::MenulistButton: 83 return true; 84 case StyleAppearance::Menulist: 85 return !IsThemed(disp) || 86 PresContext()->Theme()->ThemeNeedsComboboxDropmarker(); 87 default: 88 return false; 89 } 90 } 91 92 nscoord nsComboboxControlFrame::DropDownButtonISize() { 93 if (!HasDropDownButton()) { 94 return 0; 95 } 96 97 nsPresContext* pc = PresContext(); 98 LayoutDeviceIntSize dropdownButtonSize = pc->Theme()->GetMinimumWidgetSize( 99 pc, this, StyleAppearance::MozMenulistArrowButton); 100 return pc->DevPixelsToAppUnits(dropdownButtonSize.width); 101 } 102 103 int32_t nsComboboxControlFrame::CharCountOfLargestOptionForInflation() const { 104 uint32_t maxLength = 0; 105 nsAutoString label; 106 for (auto i : IntegerRange(Select().Options()->Length())) { 107 GetOptionText(i, label); 108 maxLength = std::max( 109 maxLength, 110 nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression( 111 label, StyleText())); 112 } 113 if (MOZ_UNLIKELY(maxLength > uint32_t(INT32_MAX))) { 114 return INT32_MAX; 115 } 116 return int32_t(maxLength); 117 } 118 119 nscoord nsComboboxControlFrame::GetOptionISize(gfxContext* aRenderingContext, 120 Type aType) const { 121 // Compute the width of each option's (potentially text-transformed) text, 122 // and use the widest one as part of our intrinsic size. 123 nscoord maxOptionSize = 0; 124 nsAutoString label; 125 nsAutoString transformedLabel; 126 RefPtr<nsFontMetrics> fm = 127 nsLayoutUtils::GetInflatedFontMetricsForFrame(this); 128 const nsStyleText* textStyle = StyleText(); 129 auto textTransform = textStyle->mTextTransform.IsNone() 130 ? Nothing() 131 : Some(textStyle->mTextTransform); 132 nsAtom* language = StyleFont()->mLanguage; 133 AutoTArray<bool, 50> charsToMergeArray; 134 AutoTArray<bool, 50> deletedCharsArray; 135 auto GetOptionSize = [&](uint32_t aIndex) -> nscoord { 136 GetOptionText(aIndex, label); 137 const nsAutoString* stringToUse = &label; 138 if (textTransform || 139 textStyle->mWebkitTextSecurity != StyleTextSecurity::None) { 140 transformedLabel.Truncate(); 141 charsToMergeArray.SetLengthAndRetainStorage(0); 142 deletedCharsArray.SetLengthAndRetainStorage(0); 143 nsCaseTransformTextRunFactory::TransformString( 144 label, transformedLabel, textTransform, 145 textStyle->TextSecurityMaskChar(), 146 /* aCaseTransformsOnly = */ false, language, charsToMergeArray, 147 deletedCharsArray); 148 stringToUse = &transformedLabel; 149 } 150 return nsLayoutUtils::AppUnitWidthOfStringBidi(*stringToUse, this, *fm, 151 *aRenderingContext); 152 }; 153 if (aType == Type::Longest) { 154 for (auto i : IntegerRange(Select().Options()->Length())) { 155 maxOptionSize = std::max(maxOptionSize, GetOptionSize(i)); 156 } 157 } else { 158 maxOptionSize = GetOptionSize(mDisplayedIndex); 159 } 160 if (maxOptionSize) { 161 // HACK: Add one app unit to workaround silly Netgear router styling, see 162 // bug 1769580. In practice since this comes from font metrics is unlikely 163 // to be perceivable. 164 maxOptionSize += 1; 165 } 166 return maxOptionSize; 167 } 168 169 nscoord nsComboboxControlFrame::IntrinsicISize(const IntrinsicSizeInput& aInput, 170 IntrinsicISizeType aType) { 171 Maybe<nscoord> containISize = ContainIntrinsicISize(NS_UNCONSTRAINEDSIZE); 172 if (containISize && *containISize != NS_UNCONSTRAINEDSIZE) { 173 return *containISize; 174 } 175 176 nscoord displayISize = 0; 177 if (!containISize) { 178 auto optionType = StyleUIReset()->mFieldSizing == StyleFieldSizing::Content 179 ? Type::Current 180 : Type::Longest; 181 displayISize += GetOptionISize(aInput.mContext, optionType); 182 } 183 184 // Add room for the dropmarker button (if there is one). 185 displayISize += DropDownButtonISize(); 186 return displayISize; 187 } 188 189 dom::HTMLSelectElement& nsComboboxControlFrame::Select() const { 190 return *static_cast<dom::HTMLSelectElement*>(GetContent()); 191 } 192 193 void nsComboboxControlFrame::GetOptionText(uint32_t aIndex, 194 nsAString& aText) const { 195 aText.Truncate(); 196 if (Element* el = Select().Options()->GetElementAt(aIndex)) { 197 static_cast<dom::HTMLOptionElement*>(el)->GetRenderedLabel(aText); 198 } 199 } 200 201 void nsComboboxControlFrame::Reflow(nsPresContext* aPresContext, 202 ReflowOutput& aDesiredSize, 203 const ReflowInput& aReflowInput, 204 nsReflowStatus& aStatus) { 205 // We don't call MarkInReflow() here; that happens in our superclass's 206 // implementation of Reflow (which we invoke further down). 207 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); 208 // Constraints we try to satisfy: 209 210 // 1) Default inline size of button is the vertical scrollbar size 211 // 2) If the inline size of button is bigger than our inline size, set 212 // inline size of button to 0. 213 // 3) Default block size of button is block size of display area 214 // 4) Inline size of display area is whatever is left over from our 215 // inline size after allocating inline size for the button. 216 WritingMode wm = aReflowInput.GetWritingMode(); 217 218 // Check if the theme specifies a minimum size for the dropdown button 219 // first. 220 const nscoord buttonISize = DropDownButtonISize(); 221 const auto padding = aReflowInput.ComputedLogicalPadding(wm); 222 223 // We ignore inline-end-padding (by adding it to our label box size) if we 224 // have a dropdown button, so that the button aligns with the end of the 225 // padding box. 226 mDisplayISize = aReflowInput.ComputedISize() - buttonISize; 227 if (buttonISize) { 228 mDisplayISize += padding.IEnd(wm); 229 } 230 231 ButtonControlFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); 232 } 233 234 void nsComboboxControlFrame::Init(nsIContent* aContent, 235 nsContainerFrame* aParent, 236 nsIFrame* aPrevInFlow) { 237 ButtonControlFrame::Init(aContent, aParent, aPrevInFlow); 238 mEventListener = new HTMLSelectEventListener( 239 Select(), HTMLSelectEventListener::SelectType::Combobox); 240 mDisplayedIndex = Select().SelectedIndex(); 241 } 242 243 nsresult nsComboboxControlFrame::RedisplaySelectedText() { 244 nsAutoScriptBlocker scriptBlocker; 245 mDisplayedIndex = Select().SelectedIndex(); 246 return RedisplayText(); 247 } 248 249 nsresult nsComboboxControlFrame::RedisplayText() { 250 nsAutoString currentLabel; 251 mDisplayLabel->GetFirstChild()->AsText()->GetData(currentLabel); 252 253 nsAutoString newLabel; 254 GetLabelText(newLabel); 255 256 // Revoke outstanding events to avoid out-of-order events which could mean 257 // displaying the wrong text. 258 mRedisplayTextEvent.Revoke(); 259 260 if (currentLabel == newLabel) { 261 return NS_OK; 262 } 263 264 NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(), 265 "If we happen to run our redisplay event now, we might kill " 266 "ourselves!"); 267 mRedisplayTextEvent = new RedisplayTextEvent(this); 268 nsContentUtils::AddScriptRunner(mRedisplayTextEvent.get()); 269 return NS_OK; 270 } 271 272 void nsComboboxControlFrame::UpdateLabelText() { 273 RefPtr<dom::Text> displayContent = mDisplayLabel->GetFirstChild()->AsText(); 274 nsAutoString newLabel; 275 GetLabelText(newLabel); 276 displayContent->SetText(newLabel, true); 277 } 278 279 void nsComboboxControlFrame::HandleRedisplayTextEvent() { 280 // First, make sure that the content model is up to date and we've constructed 281 // the frames for all our content in the right places. Otherwise they'll end 282 // up under the wrong insertion frame when we UpdateLabel, since that 283 // flushes out the content sink by calling SetText on a DOM node with aNotify 284 // set to true. See bug 289730. 285 AutoWeakFrame weakThis(this); 286 PresContext()->Document()->FlushPendingNotifications( 287 FlushType::ContentAndNotify); 288 if (!weakThis.IsAlive()) { 289 return; 290 } 291 mRedisplayTextEvent.Forget(); 292 UpdateLabelText(); 293 // Note: `this` might be dead here. 294 } 295 296 void nsComboboxControlFrame::GetLabelText(nsAString& aLabel) { 297 Select().GetPreviewValue(aLabel); 298 // Get the text to display 299 if (!aLabel.IsEmpty()) { 300 return; 301 } 302 if (mDisplayedIndex != -1) { 303 GetOptionText(mDisplayedIndex, aLabel); 304 } 305 EnsureNonEmptyLabel(aLabel); 306 } 307 308 bool nsComboboxControlFrame::IsDroppedDown() const { 309 return Select().OpenInParentProcess(); 310 } 311 312 //---------------------------------------------------------------------- 313 // nsISelectControlFrame 314 //---------------------------------------------------------------------- 315 NS_IMETHODIMP 316 nsComboboxControlFrame::DoneAddingChildren(bool aIsDone) { return NS_OK; } 317 318 NS_IMETHODIMP 319 nsComboboxControlFrame::AddOption(int32_t aIndex) { 320 if (aIndex <= mDisplayedIndex) { 321 ++mDisplayedIndex; 322 } 323 324 return NS_OK; 325 } 326 327 NS_IMETHODIMP 328 nsComboboxControlFrame::RemoveOption(int32_t aIndex) { 329 if (Select().Options()->Length()) { 330 if (aIndex < mDisplayedIndex) { 331 --mDisplayedIndex; 332 } else if (aIndex == mDisplayedIndex) { 333 mDisplayedIndex = 0; // IE6 compat 334 RedisplayText(); 335 } 336 } else { 337 // If we removed the last option, we need to blank things out 338 mDisplayedIndex = -1; 339 RedisplayText(); 340 } 341 return NS_OK; 342 } 343 344 NS_IMETHODIMP_(void) 345 nsComboboxControlFrame::OnSetSelectedIndex(int32_t aOldIndex, 346 int32_t aNewIndex) { 347 nsAutoScriptBlocker scriptBlocker; 348 mDisplayedIndex = aNewIndex; 349 RedisplayText(); 350 } 351 352 // End nsISelectControlFrame 353 //---------------------------------------------------------------------- 354 355 nsresult nsComboboxControlFrame::HandleEvent(nsPresContext* aPresContext, 356 WidgetGUIEvent* aEvent, 357 nsEventStatus* aEventStatus) { 358 NS_ENSURE_ARG_POINTER(aEventStatus); 359 360 if (nsEventStatus_eConsumeNoDefault == *aEventStatus) { 361 return NS_OK; 362 } 363 364 return ButtonControlFrame::HandleEvent(aPresContext, aEvent, aEventStatus); 365 } 366 367 nsresult nsComboboxControlFrame::CreateAnonymousContent( 368 nsTArray<ContentInfo>& aElements) { 369 dom::Document* doc = mContent->OwnerDoc(); 370 mDisplayLabel = doc->CreateHTMLElement(nsGkAtoms::label); 371 { 372 RefPtr<nsTextNode> text = doc->CreateEmptyTextNode(); 373 mDisplayLabel->AppendChildTo(text, false, IgnoreErrors()); 374 // set the value of the text node 375 UpdateLabelText(); 376 } 377 aElements.AppendElement(mDisplayLabel); 378 379 if (HasDropDownButton()) { 380 mButtonContent = mContent->OwnerDoc()->CreateHTMLElement(nsGkAtoms::button); 381 { 382 // This gives the button a reasonable height. This could be done via CSS 383 // instead, but relative font units like 1lh don't play very well with our 384 // font inflation implementation, so we do it this way instead. 385 RefPtr<nsTextNode> text = doc->CreateTextNode(u"\ufeff"_ns); 386 mButtonContent->AppendChildTo(text, false, IgnoreErrors()); 387 } 388 // Make someone to listen to the button. 389 mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::type, u"button"_ns, 390 false); 391 // Set tabindex="-1" so that the button is not tabbable 392 mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns, 393 false); 394 aElements.AppendElement(mButtonContent); 395 } 396 397 return NS_OK; 398 } 399 400 void nsComboboxControlFrame::AppendAnonymousContentTo( 401 nsTArray<nsIContent*>& aElements, uint32_t aFilter) { 402 if (mDisplayLabel) { 403 aElements.AppendElement(mDisplayLabel); 404 } 405 406 if (mButtonContent) { 407 aElements.AppendElement(mButtonContent); 408 } 409 } 410 411 namespace mozilla { 412 413 class ComboboxLabelFrame final : public nsBlockFrame { 414 public: 415 NS_DECL_QUERYFRAME 416 NS_DECL_FRAMEARENA_HELPERS(ComboboxLabelFrame) 417 418 #ifdef DEBUG_FRAME_DUMP 419 nsresult GetFrameName(nsAString& aResult) const final { 420 return MakeFrameName(u"ComboboxLabel"_ns, aResult); 421 } 422 #endif 423 424 void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, 425 const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final; 426 427 public: 428 ComboboxLabelFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) 429 : nsBlockFrame(aStyle, aPresContext, kClassID) {} 430 }; 431 432 NS_QUERYFRAME_HEAD(ComboboxLabelFrame) 433 NS_QUERYFRAME_ENTRY(ComboboxLabelFrame) 434 NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) 435 NS_IMPL_FRAMEARENA_HELPERS(ComboboxLabelFrame) 436 437 void ComboboxLabelFrame::Reflow(nsPresContext* aPresContext, 438 ReflowOutput& aDesiredSize, 439 const ReflowInput& aReflowInput, 440 nsReflowStatus& aStatus) { 441 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); 442 443 const nsComboboxControlFrame* combobox = do_QueryFrame(GetParent()); 444 MOZ_ASSERT(combobox, "Combobox's frame tree is wrong!"); 445 MOZ_ASSERT(aReflowInput.ComputedPhysicalBorderPadding() == nsMargin(), 446 "We shouldn't have border and padding in UA!"); 447 448 ReflowInput state(aReflowInput); 449 state.SetComputedISize(combobox->mDisplayISize); 450 nsBlockFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); 451 aStatus.Reset(); // this type of frame can't be split 452 } 453 454 } // namespace mozilla 455 456 nsIFrame* NS_NewComboboxLabelFrame(PresShell* aPresShell, 457 ComputedStyle* aStyle) { 458 return new (aPresShell) 459 ComboboxLabelFrame(aStyle, aPresShell->GetPresContext()); 460 } 461 462 void nsComboboxControlFrame::Destroy(DestroyContext& aContext) { 463 // Revoke any pending RedisplayTextEvent 464 mRedisplayTextEvent.Revoke(); 465 mEventListener->Detach(); 466 467 aContext.AddAnonymousContent(mDisplayLabel.forget()); 468 aContext.AddAnonymousContent(mButtonContent.forget()); 469 ButtonControlFrame::Destroy(aContext); 470 } 471 472 //--------------------------------------------------------- 473 // gets the content (an option) by index and then set it as 474 // being selected or not selected 475 //--------------------------------------------------------- 476 NS_IMETHODIMP 477 nsComboboxControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) { 478 if (aSelected) { 479 nsAutoScriptBlocker blocker; 480 mDisplayedIndex = aIndex; 481 RedisplayText(); 482 } else { 483 AutoWeakFrame weakFrame(this); 484 RedisplaySelectedText(); 485 if (weakFrame.IsAlive()) { 486 FireValueChangeEvent(); // Fire after old option is unselected 487 } 488 } 489 return NS_OK; 490 } 491 492 void nsComboboxControlFrame::FireValueChangeEvent() { 493 // Fire ValueChange event to indicate data value of combo box has changed 494 // FIXME(emilio): This shouldn't be exposed to content. 495 nsContentUtils::AddScriptRunner(new AsyncEventDispatcher( 496 mContent, u"ValueChange"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo)); 497 }