nsListControlFrame.cpp (37592B)
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 "nsListControlFrame.h" 8 9 #include <algorithm> 10 11 #include "HTMLSelectEventListener.h" 12 #include "mozilla/Attributes.h" 13 #include "mozilla/EventStateManager.h" 14 #include "mozilla/LookAndFeel.h" 15 #include "mozilla/MouseEvents.h" 16 #include "mozilla/Preferences.h" 17 #include "mozilla/PresShell.h" 18 #include "mozilla/StaticPrefs_browser.h" 19 #include "mozilla/StaticPrefs_ui.h" 20 #include "mozilla/TextEvents.h" 21 #include "mozilla/dom/Event.h" 22 #include "mozilla/dom/HTMLOptGroupElement.h" 23 #include "mozilla/dom/HTMLOptionsCollection.h" 24 #include "mozilla/dom/HTMLSelectElement.h" 25 #include "mozilla/dom/MouseEvent.h" 26 #include "mozilla/dom/MouseEventBinding.h" 27 #include "nsCOMPtr.h" 28 #include "nsCSSRendering.h" 29 #include "nsComboboxControlFrame.h" 30 #include "nsContentUtils.h" 31 #include "nsDisplayList.h" 32 #include "nsFontMetrics.h" 33 #include "nsGkAtoms.h" 34 #include "nsLayoutUtils.h" 35 #include "nsUnicharUtils.h" 36 #include "nscore.h" 37 38 using namespace mozilla; 39 using namespace mozilla::dom; 40 41 //--------------------------------------------------------- 42 nsListControlFrame* NS_NewListControlFrame(PresShell* aPresShell, 43 ComputedStyle* aStyle) { 44 return new (aPresShell) 45 nsListControlFrame(aStyle, aPresShell->GetPresContext()); 46 } 47 48 NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame) 49 50 nsListControlFrame::nsListControlFrame(ComputedStyle* aStyle, 51 nsPresContext* aPresContext) 52 : ScrollContainerFrame(aStyle, aPresContext, kClassID, false), 53 mChangesSinceDragStart(false), 54 mIsAllContentHere(false), 55 mIsAllFramesHere(false), 56 mHasBeenInitialized(false), 57 mNeedToReset(true), 58 mPostChildrenLoadedReset(false), 59 mMightNeedSecondPass(false), 60 mReflowWasInterrupted(false) {} 61 62 nsListControlFrame::~nsListControlFrame() = default; 63 64 Maybe<nscoord> nsListControlFrame::GetNaturalBaselineBOffset( 65 WritingMode aWM, BaselineSharingGroup aBaselineGroup, 66 BaselineExportContext) const { 67 // Unlike scroll frames which we inherit from, we don't export a baseline. 68 return Nothing{}; 69 } 70 // for Bug 47302 (remove this comment later) 71 void nsListControlFrame::Destroy(DestroyContext& aContext) { 72 // get the receiver interface from the browser button's content node 73 NS_ENSURE_TRUE_VOID(mContent); 74 75 // Clear the frame pointer on our event listener, just in case the 76 // event listener can outlive the frame. 77 78 mEventListener->Detach(); 79 ScrollContainerFrame::Destroy(aContext); 80 } 81 82 HTMLOptionElement* nsListControlFrame::GetCurrentOption() const { 83 return mEventListener->GetCurrentOption(); 84 } 85 86 bool nsListControlFrame::IsFocused() const { 87 return Select().State().HasState(ElementState::FOCUS); 88 } 89 90 void nsListControlFrame::InvalidateFocus() { InvalidateFrame(); } 91 92 NS_QUERYFRAME_HEAD(nsListControlFrame) 93 NS_QUERYFRAME_ENTRY(nsISelectControlFrame) 94 NS_QUERYFRAME_ENTRY(nsListControlFrame) 95 NS_QUERYFRAME_TAIL_INHERITING(ScrollContainerFrame) 96 97 #ifdef ACCESSIBILITY 98 a11y::AccType nsListControlFrame::AccessibleType() { 99 return a11y::eHTMLSelectListType; 100 } 101 #endif 102 103 // Return true if we found at least one <option> or non-empty <optgroup> label 104 // that has a frame. aResult will be the maximum BSize of those. 105 static bool GetMaxRowBSize(nsIFrame* aContainer, WritingMode aWM, 106 nscoord* aResult) { 107 bool found = false; 108 for (nsIFrame* child : aContainer->PrincipalChildList()) { 109 if (child->GetContent()->IsHTMLElement(nsGkAtoms::optgroup)) { 110 // An optgroup; drill through any scroll frame and recurse. |inner| might 111 // be null here though if |inner| is an anonymous leaf frame of some sort. 112 auto inner = child->GetContentInsertionFrame(); 113 if (inner && GetMaxRowBSize(inner, aWM, aResult)) { 114 found = true; 115 } 116 } else { 117 // an option or optgroup label 118 bool isOptGroupLabel = 119 child->Style()->IsPseudoElement() && 120 aContainer->GetContent()->IsHTMLElement(nsGkAtoms::optgroup); 121 nscoord childBSize = child->BSize(aWM); 122 // XXX bug 1499176: skip empty <optgroup> labels (zero bsize) for now 123 if (!isOptGroupLabel || childBSize > nscoord(0)) { 124 found = true; 125 *aResult = std::max(childBSize, *aResult); 126 } 127 } 128 } 129 return found; 130 } 131 132 //----------------------------------------------------------------- 133 // Main Reflow for ListBox/Dropdown 134 //----------------------------------------------------------------- 135 136 nscoord nsListControlFrame::CalcBSizeOfARow() { 137 // Calculate the block size in our writing mode of a single row in the 138 // listbox or dropdown list by using the tallest thing in the subtree, 139 // since there may be option groups in addition to option elements, 140 // either of which may be visible or invisible, may use different 141 // fonts, etc. 142 nscoord rowBSize(0); 143 if (GetContainSizeAxes().mBContained || 144 !GetMaxRowBSize(GetContentInsertionFrame(), GetWritingMode(), 145 &rowBSize)) { 146 // We don't have any <option>s or <optgroup> labels with a frame. 147 // (Or we're size-contained in block axis, which has the same outcome for 148 // our sizing.) 149 float inflation = nsLayoutUtils::FontSizeInflationFor(this); 150 rowBSize = CalcFallbackRowBSize(inflation); 151 } 152 return rowBSize; 153 } 154 155 nscoord nsListControlFrame::IntrinsicISize(const IntrinsicSizeInput& aInput, 156 IntrinsicISizeType aType) { 157 // Always add scrollbar inline sizes to the intrinsic isize of the 158 // scrolled content. Combobox frames depend on this happening in the 159 // dropdown, and standalone listboxes are overflow:scroll so they need 160 // it too. 161 WritingMode wm = GetWritingMode(); 162 nscoord result; 163 if (Maybe<nscoord> containISize = ContainIntrinsicISize()) { 164 result = *containISize; 165 } else { 166 result = GetScrolledFrame()->IntrinsicISize(aInput, aType); 167 } 168 LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes()); 169 result = NSCoordSaturatingAdd(result, scrollbarSize.IStartEnd(wm)); 170 return result; 171 } 172 173 void nsListControlFrame::Reflow(nsPresContext* aPresContext, 174 ReflowOutput& aDesiredSize, 175 const ReflowInput& aReflowInput, 176 nsReflowStatus& aStatus) { 177 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); 178 NS_WARNING_ASSERTION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE, 179 "Must have a computed inline size"); 180 181 const bool hadPendingInterrupt = aPresContext->HasPendingInterrupt(); 182 183 SchedulePaint(); 184 185 // If all the content and frames are here 186 // then initialize it before reflow 187 if (mIsAllContentHere && !mHasBeenInitialized) { 188 if (!mIsAllFramesHere) { 189 CheckIfAllFramesHere(); 190 } 191 if (mIsAllFramesHere && !mHasBeenInitialized) { 192 mHasBeenInitialized = true; 193 } 194 } 195 196 MarkInReflow(); 197 // Due to the fact that our intrinsic block size depends on the block 198 // sizes of our kids, we end up having to do two-pass reflow, in 199 // general -- the first pass to find the intrinsic block size and a 200 // second pass to reflow the scrollframe at that block size (which 201 // will size the scrollbars correctly, etc). 202 // 203 // Naturally, we want to avoid doing the second reflow as much as 204 // possible. We can skip it in the following cases (in all of which the first 205 // reflow is already happening at the right block size): 206 bool autoBSize = (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE); 207 Maybe<nscoord> containBSize = ContainIntrinsicBSize(NS_UNCONSTRAINEDSIZE); 208 bool usingContainBSize = 209 autoBSize && containBSize && *containBSize != NS_UNCONSTRAINEDSIZE; 210 211 mMightNeedSecondPass = [&] { 212 if (!autoBSize) { 213 // We're reflowing with a constrained computed block size -- just use that 214 // block size. 215 return false; 216 } 217 if (!IsSubtreeDirty() && !aReflowInput.ShouldReflowAllKids()) { 218 // We're not dirty and have no dirty kids and shouldn't be reflowing all 219 // kids. In this case, our cached max block size of a child is not going 220 // to change. 221 return false; 222 } 223 if (usingContainBSize) { 224 // We're size-contained in the block axis. In this case the size of a row 225 // doesn't depend on our children (it's the "fallback" size). 226 return false; 227 } 228 // We might need to do a second pass. If we do our first reflow using our 229 // cached max block size of a child, then compute the new max block size, 230 // and it's the same as the old one, we might still skip it (see the 231 // IsScrollbarUpdateSuppressed() check). 232 return true; 233 }(); 234 235 ReflowInput state(aReflowInput); 236 int32_t length = GetNumberOfRows(); 237 238 nscoord oldBSizeOfARow = BSizeOfARow(); 239 240 if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && autoBSize) { 241 // When not doing an initial reflow, and when the block size is 242 // auto, start off with our computed block size set to what we'd 243 // expect our block size to be. 244 nscoord computedBSize = CalcIntrinsicBSize(oldBSizeOfARow, length); 245 computedBSize = state.ApplyMinMaxBSize(computedBSize); 246 state.SetComputedBSize(computedBSize); 247 } 248 249 if (usingContainBSize) { 250 state.SetComputedBSize(*containBSize); 251 } 252 253 ScrollContainerFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); 254 255 mBSizeOfARow = CalcBSizeOfARow(); 256 257 if (!mMightNeedSecondPass) { 258 NS_ASSERTION( 259 !autoBSize || usingContainBSize || BSizeOfARow() == oldBSizeOfARow, 260 "How did our BSize of a row change if nothing was dirty?"); 261 NS_ASSERTION(!autoBSize || usingContainBSize || 262 !HasAnyStateBits(NS_FRAME_FIRST_REFLOW), 263 "How do we not need a second pass during initial reflow at " 264 "auto BSize?"); 265 if (!autoBSize || usingContainBSize) { 266 // Update our mNumDisplayRows based on our new row block size now 267 // that we know it. Note that if autoBSize and we landed in this 268 // code then we already set mNumDisplayRows in CalcIntrinsicBSize. 269 // Also note that we can't use BSizeOfARow() here because that 270 // just uses a cached value that we didn't compute. 271 nscoord rowBSize = CalcBSizeOfARow(); 272 if (rowBSize == 0) { 273 // Just pick something 274 mNumDisplayRows = 1; 275 } else { 276 mNumDisplayRows = std::max(1, state.ComputedBSize() / rowBSize); 277 } 278 } 279 280 return; 281 } 282 283 mMightNeedSecondPass = false; 284 285 // Now see whether we need a second pass. If we do, our 286 // nsSelectsAreaFrame will have suppressed the scrollbar update. 287 if (mBSizeOfARow == oldBSizeOfARow) { 288 return; 289 } 290 291 // Gotta reflow again. 292 // XXXbz We're just changing the block size here; do we need to dirty 293 // ourselves or anything like that? We might need to, per the letter 294 // of the reflow protocol, but things seem to work fine without it... 295 // Is that just an implementation detail of ScrollContainerFrame that 296 // we're depending on? 297 ScrollContainerFrame::DidReflow(aPresContext, &state); 298 299 // Now compute the block size we want to have 300 nscoord computedBSize = CalcIntrinsicBSize(BSizeOfARow(), length); 301 computedBSize = state.ApplyMinMaxBSize(computedBSize); 302 state.SetComputedBSize(computedBSize); 303 304 // XXXbz to make the ascent really correct, we should add our 305 // mComputedPadding.top to it (and subtract it from descent). Need that 306 // because ScrollContainerFrame just adds in the border.... 307 aStatus.Reset(); 308 ScrollContainerFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); 309 310 mReflowWasInterrupted |= 311 !hadPendingInterrupt && aPresContext->HasPendingInterrupt(); 312 } 313 314 bool nsListControlFrame::ShouldPropagateComputedBSizeToScrolledContent() const { 315 return true; 316 } 317 318 //--------------------------------------------------------- 319 bool nsListControlFrame::ExtendedSelection(int32_t aStartIndex, 320 int32_t aEndIndex, bool aClearAll) { 321 return SetOptionsSelectedFromFrame(aStartIndex, aEndIndex, true, aClearAll); 322 } 323 324 //--------------------------------------------------------- 325 bool nsListControlFrame::SingleSelection(int32_t aClickedIndex, 326 bool aDoToggle) { 327 #ifdef ACCESSIBILITY 328 nsCOMPtr<nsIContent> prevOption = mEventListener->GetCurrentOption(); 329 #endif 330 bool wasChanged = false; 331 // Get Current selection 332 if (aDoToggle) { 333 wasChanged = ToggleOptionSelectedFromFrame(aClickedIndex); 334 } else { 335 wasChanged = 336 SetOptionsSelectedFromFrame(aClickedIndex, aClickedIndex, true, true); 337 } 338 AutoWeakFrame weakFrame(this); 339 ScrollToIndex(aClickedIndex); 340 if (!weakFrame.IsAlive()) { 341 return wasChanged; 342 } 343 344 mStartSelectionIndex = aClickedIndex; 345 mEndSelectionIndex = aClickedIndex; 346 InvalidateFocus(); 347 348 #ifdef ACCESSIBILITY 349 FireMenuItemActiveEvent(prevOption); 350 #endif 351 352 return wasChanged; 353 } 354 355 void nsListControlFrame::InitSelectionRange(int32_t aClickedIndex) { 356 // 357 // If nothing is selected, set the start selection depending on where 358 // the user clicked and what the initial selection is: 359 // - if the user clicked *before* selectedIndex, set the start index to 360 // the end of the first contiguous selection. 361 // - if the user clicked *after* the end of the first contiguous 362 // selection, set the start index to selectedIndex. 363 // - if the user clicked *within* the first contiguous selection, set the 364 // start index to selectedIndex. 365 // The last two rules, of course, boil down to the same thing: if the user 366 // clicked >= selectedIndex, return selectedIndex. 367 // 368 // This makes it so that shift click works properly when you first click 369 // in a multiple select. 370 // 371 int32_t selectedIndex = GetSelectedIndex(); 372 if (selectedIndex >= 0) { 373 // Get the end of the contiguous selection 374 RefPtr<dom::HTMLOptionsCollection> options = GetOptions(); 375 NS_ASSERTION(options, "Collection of options is null!"); 376 uint32_t numOptions = options->Length(); 377 // Push i to one past the last selected index in the group. 378 uint32_t i; 379 for (i = selectedIndex + 1; i < numOptions; i++) { 380 if (!options->ItemAsOption(i)->Selected()) { 381 break; 382 } 383 } 384 385 if (aClickedIndex < selectedIndex) { 386 // User clicked before selection, so start selection at end of 387 // contiguous selection 388 mStartSelectionIndex = i - 1; 389 mEndSelectionIndex = selectedIndex; 390 } else { 391 // User clicked after selection, so start selection at start of 392 // contiguous selection 393 mStartSelectionIndex = selectedIndex; 394 mEndSelectionIndex = i - 1; 395 } 396 } 397 } 398 399 static uint32_t CountOptionsAndOptgroups(nsIFrame* aFrame) { 400 uint32_t count = 0; 401 for (nsIFrame* child : aFrame->PrincipalChildList()) { 402 nsIContent* content = child->GetContent(); 403 if (content) { 404 if (content->IsHTMLElement(nsGkAtoms::option)) { 405 ++count; 406 } else { 407 RefPtr<HTMLOptGroupElement> optgroup = 408 HTMLOptGroupElement::FromNode(content); 409 if (optgroup) { 410 nsAutoString label; 411 optgroup->GetLabel(label); 412 if (label.Length() > 0) { 413 ++count; 414 } 415 count += CountOptionsAndOptgroups(child); 416 } 417 } 418 } 419 } 420 return count; 421 } 422 423 uint32_t nsListControlFrame::GetNumberOfRows() { 424 return ::CountOptionsAndOptgroups(GetContentInsertionFrame()); 425 } 426 427 //--------------------------------------------------------- 428 bool nsListControlFrame::PerformSelection(int32_t aClickedIndex, bool aIsShift, 429 bool aIsControl) { 430 if (aClickedIndex == kNothingSelected) { 431 // Ignore kNothingSelected. 432 return false; 433 } 434 if (!GetMultiple()) { 435 return SingleSelection(aClickedIndex, false); 436 } 437 bool wasChanged = false; 438 if (aIsShift) { 439 // Make sure shift+click actually does something expected when 440 // the user has never clicked on the select 441 if (mStartSelectionIndex == kNothingSelected) { 442 InitSelectionRange(aClickedIndex); 443 } 444 445 // Get the range from beginning (low) to end (high) 446 // Shift *always* works, even if the current option is disabled 447 int32_t startIndex; 448 int32_t endIndex; 449 if (mStartSelectionIndex == kNothingSelected) { 450 startIndex = aClickedIndex; 451 endIndex = aClickedIndex; 452 } else if (mStartSelectionIndex <= aClickedIndex) { 453 startIndex = mStartSelectionIndex; 454 endIndex = aClickedIndex; 455 } else { 456 startIndex = aClickedIndex; 457 endIndex = mStartSelectionIndex; 458 } 459 460 // Clear only if control was not pressed 461 wasChanged = ExtendedSelection(startIndex, endIndex, !aIsControl); 462 AutoWeakFrame weakFrame(this); 463 ScrollToIndex(aClickedIndex); 464 if (!weakFrame.IsAlive()) { 465 return wasChanged; 466 } 467 468 if (mStartSelectionIndex == kNothingSelected) { 469 mStartSelectionIndex = aClickedIndex; 470 } 471 #ifdef ACCESSIBILITY 472 nsCOMPtr<nsIContent> prevOption = GetCurrentOption(); 473 #endif 474 mEndSelectionIndex = aClickedIndex; 475 InvalidateFocus(); 476 477 #ifdef ACCESSIBILITY 478 FireMenuItemActiveEvent(prevOption); 479 #endif 480 } else if (aIsControl) { 481 wasChanged = SingleSelection(aClickedIndex, true); // might destroy us 482 } else { 483 wasChanged = SingleSelection(aClickedIndex, false); // might destroy us 484 } 485 return wasChanged; 486 } 487 488 //--------------------------------------------------------- 489 bool nsListControlFrame::HandleListSelection(dom::Event* aEvent, 490 int32_t aClickedIndex) { 491 MouseEvent* mouseEvent = aEvent->AsMouseEvent(); 492 bool isControl; 493 #ifdef XP_MACOSX 494 isControl = mouseEvent->MetaKey(); 495 #else 496 isControl = mouseEvent->CtrlKey(); 497 #endif 498 bool isShift = mouseEvent->ShiftKey(); 499 return PerformSelection(aClickedIndex, isShift, 500 isControl); // might destroy us 501 } 502 503 //--------------------------------------------------------- 504 void nsListControlFrame::CaptureMouseEvents(bool aGrabMouseEvents) { 505 if (aGrabMouseEvents) { 506 PresShell::SetCapturingContent(mContent, CaptureFlags::IgnoreAllowedState); 507 } else { 508 nsIContent* capturingContent = PresShell::GetCapturingContent(); 509 if (capturingContent == mContent) { 510 // only clear the capturing content if *we* are the ones doing the 511 // capturing (or if the dropdown is hidden, in which case NO-ONE should 512 // be capturing anything - it could be a scrollbar inside this listbox 513 // which is actually grabbing 514 // This shouldn't be necessary. We should simply ensure that events 515 // targeting scrollbars are never visible to DOM consumers. 516 PresShell::ReleaseCapturingContent(); 517 } 518 } 519 } 520 521 //--------------------------------------------------------- 522 nsresult nsListControlFrame::HandleEvent(nsPresContext* aPresContext, 523 WidgetGUIEvent* aEvent, 524 nsEventStatus* aEventStatus) { 525 NS_ENSURE_ARG_POINTER(aEventStatus); 526 527 /*const char * desc[] = {"eMouseMove", 528 "NS_MOUSE_LEFT_BUTTON_UP", 529 "NS_MOUSE_LEFT_BUTTON_DOWN", 530 "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>", 531 "NS_MOUSE_MIDDLE_BUTTON_UP", 532 "NS_MOUSE_MIDDLE_BUTTON_DOWN", 533 "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>", 534 "NS_MOUSE_RIGHT_BUTTON_UP", 535 "NS_MOUSE_RIGHT_BUTTON_DOWN", 536 "eMouseOver", 537 "eMouseOut", 538 "NS_MOUSE_LEFT_DOUBLECLICK", 539 "NS_MOUSE_MIDDLE_DOUBLECLICK", 540 "NS_MOUSE_RIGHT_DOUBLECLICK", 541 "NS_MOUSE_LEFT_CLICK", 542 "NS_MOUSE_MIDDLE_CLICK", 543 "NS_MOUSE_RIGHT_CLICK"}; 544 int inx = aEvent->mMessage - eMouseEventFirst; 545 if (inx >= 0 && inx <= (NS_MOUSE_RIGHT_CLICK - eMouseEventFirst)) { 546 printf("Mouse in ListFrame %s [%d]\n", desc[inx], aEvent->mMessage); 547 } else { 548 printf("Mouse in ListFrame <UNKNOWN> [%d]\n", aEvent->mMessage); 549 }*/ 550 551 if (nsEventStatus_eConsumeNoDefault == *aEventStatus) { 552 return NS_OK; 553 } 554 555 // disabled state affects how we're selected, but we don't want to go through 556 // ScrollContainerFrame if we're disabled. 557 if (IsContentDisabled()) { 558 return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); 559 } 560 561 return ScrollContainerFrame::HandleEvent(aPresContext, aEvent, aEventStatus); 562 } 563 564 //--------------------------------------------------------- 565 void nsListControlFrame::SetInitialChildList(ChildListID aListID, 566 nsFrameList&& aChildList) { 567 if (aListID == FrameChildListID::Principal) { 568 // First check to see if all the content has been added 569 mIsAllContentHere = Select().IsDoneAddingChildren(); 570 if (!mIsAllContentHere) { 571 mIsAllFramesHere = false; 572 mHasBeenInitialized = false; 573 } 574 } 575 ScrollContainerFrame::SetInitialChildList(aListID, std::move(aChildList)); 576 } 577 578 bool nsListControlFrame::GetMultiple() const { 579 return mContent->AsElement()->HasAttr(nsGkAtoms::multiple); 580 } 581 582 HTMLSelectElement& nsListControlFrame::Select() const { 583 return *static_cast<HTMLSelectElement*>(GetContent()); 584 } 585 586 //--------------------------------------------------------- 587 void nsListControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, 588 nsIFrame* aPrevInFlow) { 589 ScrollContainerFrame::Init(aContent, aParent, aPrevInFlow); 590 591 // we shouldn't have to unregister this listener because when 592 // our frame goes away all these content node go away as well 593 // because our frame is the only one who references them. 594 // we need to hook up our listeners before the editor is initialized 595 mEventListener = new HTMLSelectEventListener( 596 Select(), HTMLSelectEventListener::SelectType::Listbox); 597 598 mStartSelectionIndex = kNothingSelected; 599 mEndSelectionIndex = kNothingSelected; 600 } 601 602 dom::HTMLOptionsCollection* nsListControlFrame::GetOptions() const { 603 return Select().Options(); 604 } 605 606 dom::HTMLOptionElement* nsListControlFrame::GetOption(uint32_t aIndex) const { 607 return Select().Item(aIndex); 608 } 609 610 NS_IMETHODIMP 611 nsListControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) { 612 if (aSelected) { 613 ScrollToIndex(aIndex); 614 } 615 return NS_OK; 616 } 617 618 void nsListControlFrame::OnContentReset() { ResetList(true); } 619 620 void nsListControlFrame::ResetList(bool aAllowScrolling) { 621 // if all the frames aren't here 622 // don't bother reseting 623 if (!mIsAllFramesHere) { 624 return; 625 } 626 627 if (aAllowScrolling) { 628 mPostChildrenLoadedReset = true; 629 630 // Scroll to the selected index 631 int32_t indexToSelect = kNothingSelected; 632 633 HTMLSelectElement* selectElement = HTMLSelectElement::FromNode(mContent); 634 if (selectElement) { 635 indexToSelect = selectElement->SelectedIndex(); 636 AutoWeakFrame weakFrame(this); 637 ScrollToIndex(indexToSelect); 638 if (!weakFrame.IsAlive()) { 639 return; 640 } 641 } 642 } 643 644 mStartSelectionIndex = kNothingSelected; 645 mEndSelectionIndex = kNothingSelected; 646 InvalidateFocus(); 647 // Combobox will redisplay itself with the OnOptionSelected event 648 } 649 650 void nsListControlFrame::ElementStateChanged(ElementState aStates) { 651 if (aStates.HasState(ElementState::FOCUS)) { 652 InvalidateFocus(); 653 } 654 } 655 656 void nsListControlFrame::GetOptionText(uint32_t aIndex, nsAString& aStr) { 657 aStr.Truncate(); 658 if (dom::HTMLOptionElement* optionElement = GetOption(aIndex)) { 659 optionElement->GetRenderedLabel(aStr); 660 } 661 } 662 663 int32_t nsListControlFrame::GetSelectedIndex() { 664 dom::HTMLSelectElement* select = 665 dom::HTMLSelectElement::FromNodeOrNull(mContent); 666 return select->SelectedIndex(); 667 } 668 669 uint32_t nsListControlFrame::GetNumberOfOptions() { 670 dom::HTMLOptionsCollection* options = GetOptions(); 671 if (!options) { 672 return 0; 673 } 674 675 return options->Length(); 676 } 677 678 //---------------------------------------------------------------------- 679 // nsISelectControlFrame 680 //---------------------------------------------------------------------- 681 bool nsListControlFrame::CheckIfAllFramesHere() { 682 // XXX Need to find a fail proof way to determine that 683 // all the frames are there 684 mIsAllFramesHere = true; 685 686 // now make sure we have a frame each piece of content 687 688 return mIsAllFramesHere; 689 } 690 691 NS_IMETHODIMP 692 nsListControlFrame::DoneAddingChildren(bool aIsDone) { 693 mIsAllContentHere = aIsDone; 694 if (mIsAllContentHere) { 695 // Here we check to see if all the frames have been created 696 // for all the content. 697 // If so, then we can initialize; 698 if (!mIsAllFramesHere) { 699 // if all the frames are now present we can initialize 700 if (CheckIfAllFramesHere()) { 701 mHasBeenInitialized = true; 702 ResetList(true); 703 } 704 } 705 } 706 return NS_OK; 707 } 708 709 NS_IMETHODIMP 710 nsListControlFrame::AddOption(int32_t aIndex) { 711 if (!mIsAllContentHere) { 712 mIsAllContentHere = Select().IsDoneAddingChildren(); 713 if (!mIsAllContentHere) { 714 mIsAllFramesHere = false; 715 mHasBeenInitialized = false; 716 } else { 717 mIsAllFramesHere = 718 (aIndex == static_cast<int32_t>(GetNumberOfOptions() - 1)); 719 } 720 } 721 722 // Make sure we scroll to the selected option as needed 723 mNeedToReset = true; 724 725 if (!mHasBeenInitialized) { 726 return NS_OK; 727 } 728 729 mPostChildrenLoadedReset = mIsAllContentHere; 730 return NS_OK; 731 } 732 733 static int32_t DecrementAndClamp(int32_t aSelectionIndex, int32_t aLength) { 734 return aLength == 0 ? nsListControlFrame::kNothingSelected 735 : std::max(0, aSelectionIndex - 1); 736 } 737 738 NS_IMETHODIMP 739 nsListControlFrame::RemoveOption(int32_t aIndex) { 740 MOZ_ASSERT(aIndex >= 0, "negative <option> index"); 741 742 // Need to reset if we're a dropdown 743 if (mStartSelectionIndex != kNothingSelected) { 744 NS_ASSERTION(mEndSelectionIndex != kNothingSelected, ""); 745 int32_t numOptions = GetNumberOfOptions(); 746 // NOTE: numOptions is the new number of options whereas aIndex is the 747 // unadjusted index of the removed option (hence the <= below). 748 NS_ASSERTION(aIndex <= numOptions, "out-of-bounds <option> index"); 749 750 int32_t forward = mEndSelectionIndex - mStartSelectionIndex; 751 int32_t* low = forward >= 0 ? &mStartSelectionIndex : &mEndSelectionIndex; 752 int32_t* high = forward >= 0 ? &mEndSelectionIndex : &mStartSelectionIndex; 753 if (aIndex < *low) { 754 *low = ::DecrementAndClamp(*low, numOptions); 755 } 756 if (aIndex <= *high) { 757 *high = ::DecrementAndClamp(*high, numOptions); 758 } 759 if (forward == 0) { 760 *low = *high; 761 } 762 } else { 763 NS_ASSERTION(mEndSelectionIndex == kNothingSelected, ""); 764 } 765 766 InvalidateFocus(); 767 return NS_OK; 768 } 769 770 //--------------------------------------------------------- 771 // Set the option selected in the DOM. This method is named 772 // as it is because it indicates that the frame is the source 773 // of this event rather than the receiver. 774 bool nsListControlFrame::SetOptionsSelectedFromFrame(int32_t aStartIndex, 775 int32_t aEndIndex, 776 bool aValue, 777 bool aClearAll) { 778 using OptionFlag = HTMLSelectElement::OptionFlag; 779 RefPtr<HTMLSelectElement> selectElement = 780 HTMLSelectElement::FromNode(mContent); 781 782 HTMLSelectElement::OptionFlags mask = OptionFlag::Notify; 783 if (aValue) { 784 mask += OptionFlag::IsSelected; 785 } 786 if (aClearAll) { 787 mask += OptionFlag::ClearAll; 788 } 789 790 return selectElement->SetOptionsSelectedByIndex(aStartIndex, aEndIndex, mask); 791 } 792 793 bool nsListControlFrame::ToggleOptionSelectedFromFrame(int32_t aIndex) { 794 RefPtr<HTMLOptionElement> option = GetOption(static_cast<uint32_t>(aIndex)); 795 NS_ENSURE_TRUE(option, false); 796 797 RefPtr<HTMLSelectElement> selectElement = 798 HTMLSelectElement::FromNode(mContent); 799 800 HTMLSelectElement::OptionFlags mask = HTMLSelectElement::OptionFlag::Notify; 801 if (!option->Selected()) { 802 mask += HTMLSelectElement::OptionFlag::IsSelected; 803 } 804 805 return selectElement->SetOptionsSelectedByIndex(aIndex, aIndex, mask); 806 } 807 808 // Dispatch event and such 809 bool nsListControlFrame::UpdateSelection() { 810 if (mIsAllFramesHere) { 811 // if it's a combobox, display the new text. Note that after 812 // FireOnInputAndOnChange we might be dead, as that can run script. 813 AutoWeakFrame weakFrame(this); 814 if (mIsAllContentHere) { 815 RefPtr listener = mEventListener; 816 listener->FireOnInputAndOnChange(); 817 } 818 return weakFrame.IsAlive(); 819 } 820 return true; 821 } 822 823 NS_IMETHODIMP_(void) 824 nsListControlFrame::OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) { 825 #ifdef ACCESSIBILITY 826 nsCOMPtr<nsIContent> prevOption = GetCurrentOption(); 827 #endif 828 829 AutoWeakFrame weakFrame(this); 830 ScrollToIndex(aNewIndex); 831 if (!weakFrame.IsAlive()) { 832 return; 833 } 834 mStartSelectionIndex = aNewIndex; 835 mEndSelectionIndex = aNewIndex; 836 InvalidateFocus(); 837 838 #ifdef ACCESSIBILITY 839 if (aOldIndex != aNewIndex) { 840 FireMenuItemActiveEvent(prevOption); 841 } 842 #endif 843 } 844 845 //---------------------------------------------------------------------- 846 // End nsISelectControlFrame 847 //---------------------------------------------------------------------- 848 849 class AsyncReset final : public Runnable { 850 public: 851 AsyncReset(nsListControlFrame* aFrame, bool aScroll) 852 : Runnable("AsyncReset"), mFrame(aFrame), mScroll(aScroll) {} 853 854 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { 855 if (mFrame.IsAlive()) { 856 static_cast<nsListControlFrame*>(mFrame.GetFrame())->ResetList(mScroll); 857 } 858 return NS_OK; 859 } 860 861 private: 862 WeakFrame mFrame; 863 bool mScroll; 864 }; 865 866 bool nsListControlFrame::ReflowFinished() { 867 if (mNeedToReset && !mReflowWasInterrupted) { 868 mNeedToReset = false; 869 // Suppress scrolling to the selected element if we restored scroll 870 // history state AND the list contents have not changed since we loaded 871 // all the children AND nothing else forced us to scroll by calling 872 // ResetList(true). The latter two conditions are folded into 873 // mPostChildrenLoadedReset. 874 // 875 // The idea is that we want scroll history restoration to trump ResetList 876 // scrolling to the selected element, when the ResetList was probably only 877 // caused by content loading normally. 878 const bool scroll = !DidHistoryRestore() || mPostChildrenLoadedReset; 879 nsContentUtils::AddScriptRunner(new AsyncReset(this, scroll)); 880 } 881 mReflowWasInterrupted = false; 882 return ScrollContainerFrame::ReflowFinished(); 883 } 884 885 #ifdef DEBUG_FRAME_DUMP 886 nsresult nsListControlFrame::GetFrameName(nsAString& aResult) const { 887 return MakeFrameName(u"ListControl"_ns, aResult); 888 } 889 #endif 890 891 nscoord nsListControlFrame::GetBSizeOfARow() { return BSizeOfARow(); } 892 893 bool nsListControlFrame::IsOptionInteractivelySelectable(int32_t aIndex) const { 894 auto& select = Select(); 895 if (HTMLOptionElement* item = select.Item(aIndex)) { 896 return IsOptionInteractivelySelectable(&select, item); 897 } 898 return false; 899 } 900 901 bool nsListControlFrame::IsOptionInteractivelySelectable( 902 HTMLSelectElement* aSelect, HTMLOptionElement* aOption) { 903 return !aSelect->IsOptionDisabled(aOption) && aOption->GetPrimaryFrame(); 904 } 905 906 nscoord nsListControlFrame::CalcFallbackRowBSize(float aFontSizeInflation) { 907 RefPtr<nsFontMetrics> fontMet = 908 nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation); 909 return fontMet->MaxHeight(); 910 } 911 912 nscoord nsListControlFrame::CalcIntrinsicBSize(nscoord aBSizeOfARow, 913 int32_t aNumberOfOptions) { 914 if (Style()->StyleUIReset()->mFieldSizing == StyleFieldSizing::Content) { 915 int32_t length = GetNumberOfRows(); 916 return length * aBSizeOfARow; 917 } 918 919 mNumDisplayRows = Select().Size(); 920 if (mNumDisplayRows < 1) { 921 mNumDisplayRows = 4; 922 } 923 return mNumDisplayRows * aBSizeOfARow; 924 } 925 926 #ifdef ACCESSIBILITY 927 void nsListControlFrame::FireMenuItemActiveEvent(nsIContent* aPreviousOption) { 928 if (!IsFocused()) { 929 return; 930 } 931 932 nsIContent* optionContent = GetCurrentOption(); 933 if (aPreviousOption == optionContent) { 934 // No change 935 return; 936 } 937 938 if (aPreviousOption) { 939 FireDOMEvent(u"DOMMenuItemInactive"_ns, aPreviousOption); 940 } 941 942 if (optionContent) { 943 FireDOMEvent(u"DOMMenuItemActive"_ns, optionContent); 944 } 945 } 946 #endif 947 948 nsresult nsListControlFrame::GetIndexFromDOMEvent(dom::Event* aMouseEvent, 949 int32_t& aCurIndex) { 950 if (PresShell::GetCapturingContent() != mContent) { 951 // If we're not capturing, then ignore movement in the border 952 nsPoint pt = 953 nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aMouseEvent, this); 954 nsRect borderInnerEdge = GetScrollPortRect(); 955 if (!borderInnerEdge.Contains(pt)) { 956 return NS_ERROR_FAILURE; 957 } 958 } 959 960 RefPtr<dom::HTMLOptionElement> option; 961 for (nsCOMPtr<nsIContent> content = 962 PresContext()->EventStateManager()->GetEventTargetContent(nullptr); 963 content && !option; content = content->GetParent()) { 964 option = dom::HTMLOptionElement::FromNode(content); 965 } 966 967 if (option) { 968 aCurIndex = option->Index(); 969 MOZ_ASSERT(aCurIndex >= 0); 970 return NS_OK; 971 } 972 973 return NS_ERROR_FAILURE; 974 } 975 976 nsresult nsListControlFrame::HandleLeftButtonMouseDown( 977 dom::Event* aMouseEvent) { 978 int32_t selectedIndex; 979 if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { 980 // Handle Like List 981 CaptureMouseEvents(true); 982 AutoWeakFrame weakFrame(this); 983 bool change = 984 HandleListSelection(aMouseEvent, selectedIndex); // might destroy us 985 if (!weakFrame.IsAlive()) { 986 return NS_OK; 987 } 988 mChangesSinceDragStart = change; 989 } 990 return NS_OK; 991 } 992 993 nsresult nsListControlFrame::HandleLeftButtonMouseUp(dom::Event* aMouseEvent) { 994 if (!StyleVisibility()->IsVisible()) { 995 return NS_OK; 996 } 997 // Notify 998 if (mChangesSinceDragStart) { 999 // reset this so that future MouseUps without a prior MouseDown 1000 // won't fire onchange 1001 mChangesSinceDragStart = false; 1002 RefPtr listener = mEventListener; 1003 listener->FireOnInputAndOnChange(); 1004 // Note that `this` may be dead now, as the above call runs script. 1005 } 1006 return NS_OK; 1007 } 1008 1009 nsresult nsListControlFrame::DragMove(dom::Event* aMouseEvent) { 1010 NS_ASSERTION(aMouseEvent, "aMouseEvent is null."); 1011 1012 int32_t selectedIndex; 1013 if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { 1014 // Don't waste cycles if we already dragged over this item 1015 if (selectedIndex == mEndSelectionIndex) { 1016 return NS_OK; 1017 } 1018 MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); 1019 NS_ASSERTION(mouseEvent, "aMouseEvent is not a MouseEvent!"); 1020 bool isControl; 1021 #ifdef XP_MACOSX 1022 isControl = mouseEvent->MetaKey(); 1023 #else 1024 isControl = mouseEvent->CtrlKey(); 1025 #endif 1026 AutoWeakFrame weakFrame(this); 1027 // Turn SHIFT on when you are dragging, unless control is on. 1028 bool wasChanged = PerformSelection(selectedIndex, !isControl, isControl); 1029 if (!weakFrame.IsAlive()) { 1030 return NS_OK; 1031 } 1032 mChangesSinceDragStart = mChangesSinceDragStart || wasChanged; 1033 } 1034 return NS_OK; 1035 } 1036 1037 //---------------------------------------------------------------------- 1038 // Scroll helpers. 1039 //---------------------------------------------------------------------- 1040 void nsListControlFrame::ScrollToIndex(int32_t aIndex) { 1041 if (aIndex < 0) { 1042 // XXX shouldn't we just do nothing if we're asked to scroll to 1043 // kNothingSelected? 1044 ScrollTo(nsPoint(0, 0), ScrollMode::Instant); 1045 } else { 1046 RefPtr<dom::HTMLOptionElement> option = 1047 GetOption(AssertedCast<uint32_t>(aIndex)); 1048 if (option) { 1049 ScrollToFrame(*option); 1050 } 1051 } 1052 } 1053 1054 void nsListControlFrame::ScrollToFrame(dom::HTMLOptionElement& aOptElement) { 1055 // otherwise we find the content's frame and scroll to it 1056 if (nsIFrame* childFrame = aOptElement.GetPrimaryFrame()) { 1057 RefPtr<mozilla::PresShell> presShell = PresShell(); 1058 presShell->ScrollFrameIntoView(childFrame, Nothing(), ScrollAxis(), 1059 ScrollAxis(), 1060 ScrollFlags::ScrollOverflowHidden | 1061 ScrollFlags::ScrollFirstAncestorOnly); 1062 } 1063 } 1064 1065 void nsListControlFrame::UpdateSelectionAfterKeyEvent( 1066 int32_t aNewIndex, uint32_t aCharCode, bool aIsShift, bool aIsControlOrMeta, 1067 bool aIsControlSelectMode) { 1068 // If you hold control, but not shift, no key will actually do anything 1069 // except space. 1070 AutoWeakFrame weakFrame(this); 1071 bool wasChanged = false; 1072 if (aIsControlOrMeta && !aIsShift && aCharCode != ' ') { 1073 #ifdef ACCESSIBILITY 1074 nsCOMPtr<nsIContent> prevOption = GetCurrentOption(); 1075 #endif 1076 mStartSelectionIndex = aNewIndex; 1077 mEndSelectionIndex = aNewIndex; 1078 InvalidateFocus(); 1079 ScrollToIndex(aNewIndex); 1080 if (!weakFrame.IsAlive()) { 1081 return; 1082 } 1083 1084 #ifdef ACCESSIBILITY 1085 FireMenuItemActiveEvent(prevOption); 1086 #endif 1087 } else if (aIsControlSelectMode && aCharCode == ' ') { 1088 wasChanged = SingleSelection(aNewIndex, true); 1089 } else { 1090 wasChanged = PerformSelection(aNewIndex, aIsShift, aIsControlOrMeta); 1091 } 1092 if (wasChanged && weakFrame.IsAlive()) { 1093 // dispatch event, update combobox, etc. 1094 UpdateSelection(); 1095 } 1096 }