layout/forms/nsListControlFrame.cpp (815 lines of code) (raw):

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nscore.h" #include "nsCOMPtr.h" #include "nsUnicharUtils.h" #include "nsListControlFrame.h" #include "HTMLSelectEventListener.h" #include "nsGkAtoms.h" #include "nsComboboxControlFrame.h" #include "nsFontMetrics.h" #include "nsCSSRendering.h" #include "nsLayoutUtils.h" #include "nsDisplayList.h" #include "nsContentUtils.h" #include "mozilla/Attributes.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/HTMLOptGroupElement.h" #include "mozilla/dom/HTMLOptionsCollection.h" #include "mozilla/dom/HTMLSelectElement.h" #include "mozilla/dom/MouseEvent.h" #include "mozilla/dom/MouseEventBinding.h" #include "mozilla/EventStateManager.h" #include "mozilla/LookAndFeel.h" #include "mozilla/MouseEvents.h" #include "mozilla/Preferences.h" #include "mozilla/PresShell.h" #include "mozilla/StaticPrefs_browser.h" #include "mozilla/StaticPrefs_ui.h" #include "mozilla/TextEvents.h" #include <algorithm> using namespace mozilla; using namespace mozilla::dom; //--------------------------------------------------------- nsListControlFrame* NS_NewListControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) { nsListControlFrame* it = new (aPresShell) nsListControlFrame(aStyle, aPresShell->GetPresContext()); it->AddStateBits(NS_FRAME_INDEPENDENT_SELECTION); return it; } NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame) nsListControlFrame::nsListControlFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) : ScrollContainerFrame(aStyle, aPresContext, kClassID, false), mChangesSinceDragStart(false), mIsAllContentHere(false), mIsAllFramesHere(false), mHasBeenInitialized(false), mNeedToReset(true), mPostChildrenLoadedReset(false), mMightNeedSecondPass(false), mReflowWasInterrupted(false) {} nsListControlFrame::~nsListControlFrame() = default; Maybe<nscoord> nsListControlFrame::GetNaturalBaselineBOffset( WritingMode aWM, BaselineSharingGroup aBaselineGroup, BaselineExportContext) const { // Unlike scroll frames which we inherit from, we don't export a baseline. return Nothing{}; } // for Bug 47302 (remove this comment later) void nsListControlFrame::Destroy(DestroyContext& aContext) { // get the receiver interface from the browser button's content node NS_ENSURE_TRUE_VOID(mContent); // Clear the frame pointer on our event listener, just in case the // event listener can outlive the frame. mEventListener->Detach(); ScrollContainerFrame::Destroy(aContext); } void nsListControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { // We allow visibility:hidden <select>s to contain visible options. // Don't allow painting of list controls when painting is suppressed. // XXX why do we need this here? we should never reach this. Maybe // because these can have widgets? Hmm if (aBuilder->IsBackgroundOnly()) { return; } DO_GLOBAL_REFLOW_COUNT_DSP("nsListControlFrame"); ScrollContainerFrame::BuildDisplayList(aBuilder, aLists); } HTMLOptionElement* nsListControlFrame::GetCurrentOption() const { return mEventListener->GetCurrentOption(); } bool nsListControlFrame::IsFocused() const { return Select().State().HasState(ElementState::FOCUS); } /** * This is called by the SelectsAreaFrame, which is the same * as the frame returned by GetOptionsContainer. It's the frame which is * scrolled by us. * @param aPt the offset of this frame, relative to the rendering reference * frame */ void nsListControlFrame::PaintFocus(DrawTarget* aDrawTarget, nsPoint aPt) { if (!IsFocused()) { return; } nsIFrame* containerFrame = GetOptionsContainer(); if (!containerFrame) { return; } nsIFrame* childframe = nullptr; nsCOMPtr<nsIContent> focusedContent = GetCurrentOption(); if (focusedContent) { childframe = focusedContent->GetPrimaryFrame(); } nsRect fRect; if (childframe) { // get the child rect fRect = childframe->GetRect(); // get it into our coordinates fRect.MoveBy(childframe->GetParent()->GetOffsetTo(this)); } else { float inflation = nsLayoutUtils::FontSizeInflationFor(this); fRect.x = fRect.y = 0; if (GetWritingMode().IsVertical()) { fRect.width = GetScrollPortRect().width; fRect.height = CalcFallbackRowBSize(inflation); } else { fRect.width = CalcFallbackRowBSize(inflation); fRect.height = GetScrollPortRect().height; } fRect.MoveBy(containerFrame->GetOffsetTo(this)); } fRect += aPt; const auto* domOpt = HTMLOptionElement::FromNodeOrNull(focusedContent); const bool isSelected = domOpt && domOpt->Selected(); // Set up back stop colors and then ask L&F service for the real colors nscolor color = LookAndFeel::Color(isSelected ? LookAndFeel::ColorID::Selecteditemtext : LookAndFeel::ColorID::Selecteditem, this); nsCSSRendering::PaintFocus(PresContext(), aDrawTarget, fRect, color); } void nsListControlFrame::InvalidateFocus() { if (nsIFrame* containerFrame = GetOptionsContainer()) { containerFrame->InvalidateFrame(); } } NS_QUERYFRAME_HEAD(nsListControlFrame) NS_QUERYFRAME_ENTRY(nsISelectControlFrame) NS_QUERYFRAME_ENTRY(nsListControlFrame) NS_QUERYFRAME_TAIL_INHERITING(ScrollContainerFrame) #ifdef ACCESSIBILITY a11y::AccType nsListControlFrame::AccessibleType() { return a11y::eHTMLSelectListType; } #endif // Return true if we found at least one <option> or non-empty <optgroup> label // that has a frame. aResult will be the maximum BSize of those. static bool GetMaxRowBSize(nsIFrame* aContainer, WritingMode aWM, nscoord* aResult) { bool found = false; for (nsIFrame* child : aContainer->PrincipalChildList()) { if (child->GetContent()->IsHTMLElement(nsGkAtoms::optgroup)) { // An optgroup; drill through any scroll frame and recurse. |inner| might // be null here though if |inner| is an anonymous leaf frame of some sort. auto inner = child->GetContentInsertionFrame(); if (inner && GetMaxRowBSize(inner, aWM, aResult)) { found = true; } } else { // an option or optgroup label bool isOptGroupLabel = child->Style()->IsPseudoElement() && aContainer->GetContent()->IsHTMLElement(nsGkAtoms::optgroup); nscoord childBSize = child->BSize(aWM); // XXX bug 1499176: skip empty <optgroup> labels (zero bsize) for now if (!isOptGroupLabel || childBSize > nscoord(0)) { found = true; *aResult = std::max(childBSize, *aResult); } } } return found; } //----------------------------------------------------------------- // Main Reflow for ListBox/Dropdown //----------------------------------------------------------------- nscoord nsListControlFrame::CalcBSizeOfARow() { // Calculate the block size in our writing mode of a single row in the // listbox or dropdown list by using the tallest thing in the subtree, // since there may be option groups in addition to option elements, // either of which may be visible or invisible, may use different // fonts, etc. nscoord rowBSize(0); if (GetContainSizeAxes().mBContained || !GetMaxRowBSize(GetOptionsContainer(), GetWritingMode(), &rowBSize)) { // We don't have any <option>s or <optgroup> labels with a frame. // (Or we're size-contained in block axis, which has the same outcome for // our sizing.) float inflation = nsLayoutUtils::FontSizeInflationFor(this); rowBSize = CalcFallbackRowBSize(inflation); } return rowBSize; } nscoord nsListControlFrame::IntrinsicISize(const IntrinsicSizeInput& aInput, IntrinsicISizeType aType) { // Always add scrollbar inline sizes to the intrinsic isize of the // scrolled content. Combobox frames depend on this happening in the // dropdown, and standalone listboxes are overflow:scroll so they need // it too. WritingMode wm = GetWritingMode(); nscoord result; if (Maybe<nscoord> containISize = ContainIntrinsicISize()) { result = *containISize; } else { result = GetScrolledFrame()->IntrinsicISize(aInput, aType); } LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes()); result = NSCoordSaturatingAdd(result, scrollbarSize.IStartEnd(wm)); return result; } void nsListControlFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, const ReflowInput& aReflowInput, nsReflowStatus& aStatus) { MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); NS_WARNING_ASSERTION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE, "Must have a computed inline size"); const bool hadPendingInterrupt = aPresContext->HasPendingInterrupt(); SchedulePaint(); // If all the content and frames are here // then initialize it before reflow if (mIsAllContentHere && !mHasBeenInitialized) { if (!mIsAllFramesHere) { CheckIfAllFramesHere(); } if (mIsAllFramesHere && !mHasBeenInitialized) { mHasBeenInitialized = true; } } MarkInReflow(); // Due to the fact that our intrinsic block size depends on the block // sizes of our kids, we end up having to do two-pass reflow, in // general -- the first pass to find the intrinsic block size and a // second pass to reflow the scrollframe at that block size (which // will size the scrollbars correctly, etc). // // Naturally, we want to avoid doing the second reflow as much as // possible. We can skip it in the following cases (in all of which the first // reflow is already happening at the right block size): bool autoBSize = (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE); Maybe<nscoord> containBSize = ContainIntrinsicBSize(NS_UNCONSTRAINEDSIZE); bool usingContainBSize = autoBSize && containBSize && *containBSize != NS_UNCONSTRAINEDSIZE; mMightNeedSecondPass = [&] { if (!autoBSize) { // We're reflowing with a constrained computed block size -- just use that // block size. return false; } if (!IsSubtreeDirty() && !aReflowInput.ShouldReflowAllKids()) { // We're not dirty and have no dirty kids and shouldn't be reflowing all // kids. In this case, our cached max block size of a child is not going // to change. return false; } if (usingContainBSize) { // We're size-contained in the block axis. In this case the size of a row // doesn't depend on our children (it's the "fallback" size). return false; } // We might need to do a second pass. If we do our first reflow using our // cached max block size of a child, then compute the new max block size, // and it's the same as the old one, we might still skip it (see the // IsScrollbarUpdateSuppressed() check). return true; }(); ReflowInput state(aReflowInput); int32_t length = GetNumberOfRows(); nscoord oldBSizeOfARow = BSizeOfARow(); if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && autoBSize) { // When not doing an initial reflow, and when the block size is // auto, start off with our computed block size set to what we'd // expect our block size to be. nscoord computedBSize = CalcIntrinsicBSize(oldBSizeOfARow, length); computedBSize = state.ApplyMinMaxBSize(computedBSize); state.SetComputedBSize(computedBSize); } if (usingContainBSize) { state.SetComputedBSize(*containBSize); } ScrollContainerFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); if (!mMightNeedSecondPass) { NS_ASSERTION(!autoBSize || BSizeOfARow() == oldBSizeOfARow, "How did our BSize of a row change if nothing was dirty?"); NS_ASSERTION(!autoBSize || !HasAnyStateBits(NS_FRAME_FIRST_REFLOW) || usingContainBSize, "How do we not need a second pass during initial reflow at " "auto BSize?"); NS_ASSERTION(!IsScrollbarUpdateSuppressed(), "Shouldn't be suppressing if we don't need a second pass!"); if (!autoBSize || usingContainBSize) { // Update our mNumDisplayRows based on our new row block size now // that we know it. Note that if autoBSize and we landed in this // code then we already set mNumDisplayRows in CalcIntrinsicBSize. // Also note that we can't use BSizeOfARow() here because that // just uses a cached value that we didn't compute. nscoord rowBSize = CalcBSizeOfARow(); if (rowBSize == 0) { // Just pick something mNumDisplayRows = 1; } else { mNumDisplayRows = std::max(1, state.ComputedBSize() / rowBSize); } } return; } mMightNeedSecondPass = false; // Now see whether we need a second pass. If we do, our // nsSelectsAreaFrame will have suppressed the scrollbar update. if (!IsScrollbarUpdateSuppressed()) { // All done. No need to do more reflow. return; } SetSuppressScrollbarUpdate(false); // Gotta reflow again. // XXXbz We're just changing the block size here; do we need to dirty // ourselves or anything like that? We might need to, per the letter // of the reflow protocol, but things seem to work fine without it... // Is that just an implementation detail of ScrollContainerFrame that // we're depending on? ScrollContainerFrame::DidReflow(aPresContext, &state); // Now compute the block size we want to have nscoord computedBSize = CalcIntrinsicBSize(BSizeOfARow(), length); computedBSize = state.ApplyMinMaxBSize(computedBSize); state.SetComputedBSize(computedBSize); // XXXbz to make the ascent really correct, we should add our // mComputedPadding.top to it (and subtract it from descent). Need that // because ScrollContainerFrame just adds in the border.... aStatus.Reset(); ScrollContainerFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); mReflowWasInterrupted |= !hadPendingInterrupt && aPresContext->HasPendingInterrupt(); } bool nsListControlFrame::ShouldPropagateComputedBSizeToScrolledContent() const { return true; } //--------------------------------------------------------- nsContainerFrame* nsListControlFrame::GetContentInsertionFrame() { return GetOptionsContainer()->GetContentInsertionFrame(); } //--------------------------------------------------------- bool nsListControlFrame::ExtendedSelection(int32_t aStartIndex, int32_t aEndIndex, bool aClearAll) { return SetOptionsSelectedFromFrame(aStartIndex, aEndIndex, true, aClearAll); } //--------------------------------------------------------- bool nsListControlFrame::SingleSelection(int32_t aClickedIndex, bool aDoToggle) { #ifdef ACCESSIBILITY nsCOMPtr<nsIContent> prevOption = mEventListener->GetCurrentOption(); #endif bool wasChanged = false; // Get Current selection if (aDoToggle) { wasChanged = ToggleOptionSelectedFromFrame(aClickedIndex); } else { wasChanged = SetOptionsSelectedFromFrame(aClickedIndex, aClickedIndex, true, true); } AutoWeakFrame weakFrame(this); ScrollToIndex(aClickedIndex); if (!weakFrame.IsAlive()) { return wasChanged; } mStartSelectionIndex = aClickedIndex; mEndSelectionIndex = aClickedIndex; InvalidateFocus(); #ifdef ACCESSIBILITY FireMenuItemActiveEvent(prevOption); #endif return wasChanged; } void nsListControlFrame::InitSelectionRange(int32_t aClickedIndex) { // // If nothing is selected, set the start selection depending on where // the user clicked and what the initial selection is: // - if the user clicked *before* selectedIndex, set the start index to // the end of the first contiguous selection. // - if the user clicked *after* the end of the first contiguous // selection, set the start index to selectedIndex. // - if the user clicked *within* the first contiguous selection, set the // start index to selectedIndex. // The last two rules, of course, boil down to the same thing: if the user // clicked >= selectedIndex, return selectedIndex. // // This makes it so that shift click works properly when you first click // in a multiple select. // int32_t selectedIndex = GetSelectedIndex(); if (selectedIndex >= 0) { // Get the end of the contiguous selection RefPtr<dom::HTMLOptionsCollection> options = GetOptions(); NS_ASSERTION(options, "Collection of options is null!"); uint32_t numOptions = options->Length(); // Push i to one past the last selected index in the group. uint32_t i; for (i = selectedIndex + 1; i < numOptions; i++) { if (!options->ItemAsOption(i)->Selected()) { break; } } if (aClickedIndex < selectedIndex) { // User clicked before selection, so start selection at end of // contiguous selection mStartSelectionIndex = i - 1; mEndSelectionIndex = selectedIndex; } else { // User clicked after selection, so start selection at start of // contiguous selection mStartSelectionIndex = selectedIndex; mEndSelectionIndex = i - 1; } } } static uint32_t CountOptionsAndOptgroups(nsIFrame* aFrame) { uint32_t count = 0; for (nsIFrame* child : aFrame->PrincipalChildList()) { nsIContent* content = child->GetContent(); if (content) { if (content->IsHTMLElement(nsGkAtoms::option)) { ++count; } else { RefPtr<HTMLOptGroupElement> optgroup = HTMLOptGroupElement::FromNode(content); if (optgroup) { nsAutoString label; optgroup->GetLabel(label); if (label.Length() > 0) { ++count; } count += CountOptionsAndOptgroups(child); } } } } return count; } uint32_t nsListControlFrame::GetNumberOfRows() { return ::CountOptionsAndOptgroups(GetContentInsertionFrame()); } //--------------------------------------------------------- bool nsListControlFrame::PerformSelection(int32_t aClickedIndex, bool aIsShift, bool aIsControl) { if (aClickedIndex == kNothingSelected) { // Ignore kNothingSelected. return false; } if (!GetMultiple()) { return SingleSelection(aClickedIndex, false); } bool wasChanged = false; if (aIsShift) { // Make sure shift+click actually does something expected when // the user has never clicked on the select if (mStartSelectionIndex == kNothingSelected) { InitSelectionRange(aClickedIndex); } // Get the range from beginning (low) to end (high) // Shift *always* works, even if the current option is disabled int32_t startIndex; int32_t endIndex; if (mStartSelectionIndex == kNothingSelected) { startIndex = aClickedIndex; endIndex = aClickedIndex; } else if (mStartSelectionIndex <= aClickedIndex) { startIndex = mStartSelectionIndex; endIndex = aClickedIndex; } else { startIndex = aClickedIndex; endIndex = mStartSelectionIndex; } // Clear only if control was not pressed wasChanged = ExtendedSelection(startIndex, endIndex, !aIsControl); AutoWeakFrame weakFrame(this); ScrollToIndex(aClickedIndex); if (!weakFrame.IsAlive()) { return wasChanged; } if (mStartSelectionIndex == kNothingSelected) { mStartSelectionIndex = aClickedIndex; } #ifdef ACCESSIBILITY nsCOMPtr<nsIContent> prevOption = GetCurrentOption(); #endif mEndSelectionIndex = aClickedIndex; InvalidateFocus(); #ifdef ACCESSIBILITY FireMenuItemActiveEvent(prevOption); #endif } else if (aIsControl) { wasChanged = SingleSelection(aClickedIndex, true); // might destroy us } else { wasChanged = SingleSelection(aClickedIndex, false); // might destroy us } return wasChanged; } //--------------------------------------------------------- bool nsListControlFrame::HandleListSelection(dom::Event* aEvent, int32_t aClickedIndex) { MouseEvent* mouseEvent = aEvent->AsMouseEvent(); bool isControl; #ifdef XP_MACOSX isControl = mouseEvent->MetaKey(); #else isControl = mouseEvent->CtrlKey(); #endif bool isShift = mouseEvent->ShiftKey(); return PerformSelection(aClickedIndex, isShift, isControl); // might destroy us } //--------------------------------------------------------- void nsListControlFrame::CaptureMouseEvents(bool aGrabMouseEvents) { if (aGrabMouseEvents) { PresShell::SetCapturingContent(mContent, CaptureFlags::IgnoreAllowedState); } else { nsIContent* capturingContent = PresShell::GetCapturingContent(); if (capturingContent == mContent) { // only clear the capturing content if *we* are the ones doing the // capturing (or if the dropdown is hidden, in which case NO-ONE should // be capturing anything - it could be a scrollbar inside this listbox // which is actually grabbing // This shouldn't be necessary. We should simply ensure that events // targeting scrollbars are never visible to DOM consumers. PresShell::ReleaseCapturingContent(); } } } //--------------------------------------------------------- nsresult nsListControlFrame::HandleEvent(nsPresContext* aPresContext, WidgetGUIEvent* aEvent, nsEventStatus* aEventStatus) { NS_ENSURE_ARG_POINTER(aEventStatus); /*const char * desc[] = {"eMouseMove", "NS_MOUSE_LEFT_BUTTON_UP", "NS_MOUSE_LEFT_BUTTON_DOWN", "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>", "NS_MOUSE_MIDDLE_BUTTON_UP", "NS_MOUSE_MIDDLE_BUTTON_DOWN", "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>", "NS_MOUSE_RIGHT_BUTTON_UP", "NS_MOUSE_RIGHT_BUTTON_DOWN", "eMouseOver", "eMouseOut", "NS_MOUSE_LEFT_DOUBLECLICK", "NS_MOUSE_MIDDLE_DOUBLECLICK", "NS_MOUSE_RIGHT_DOUBLECLICK", "NS_MOUSE_LEFT_CLICK", "NS_MOUSE_MIDDLE_CLICK", "NS_MOUSE_RIGHT_CLICK"}; int inx = aEvent->mMessage - eMouseEventFirst; if (inx >= 0 && inx <= (NS_MOUSE_RIGHT_CLICK - eMouseEventFirst)) { printf("Mouse in ListFrame %s [%d]\n", desc[inx], aEvent->mMessage); } else { printf("Mouse in ListFrame <UNKNOWN> [%d]\n", aEvent->mMessage); }*/ if (nsEventStatus_eConsumeNoDefault == *aEventStatus) { return NS_OK; } // disabled state affects how we're selected, but we don't want to go through // ScrollContainerFrame if we're disabled. if (IsContentDisabled()) { return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); } return ScrollContainerFrame::HandleEvent(aPresContext, aEvent, aEventStatus); } //--------------------------------------------------------- void nsListControlFrame::SetInitialChildList(ChildListID aListID, nsFrameList&& aChildList) { if (aListID == FrameChildListID::Principal) { // First check to see if all the content has been added mIsAllContentHere = Select().IsDoneAddingChildren(); if (!mIsAllContentHere) { mIsAllFramesHere = false; mHasBeenInitialized = false; } } ScrollContainerFrame::SetInitialChildList(aListID, std::move(aChildList)); } bool nsListControlFrame::GetMultiple() const { return mContent->AsElement()->HasAttr(nsGkAtoms::multiple); } HTMLSelectElement& nsListControlFrame::Select() const { return *static_cast<HTMLSelectElement*>(GetContent()); } //--------------------------------------------------------- void nsListControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, nsIFrame* aPrevInFlow) { ScrollContainerFrame::Init(aContent, aParent, aPrevInFlow); // we shouldn't have to unregister this listener because when // our frame goes away all these content node go away as well // because our frame is the only one who references them. // we need to hook up our listeners before the editor is initialized mEventListener = new HTMLSelectEventListener( Select(), HTMLSelectEventListener::SelectType::Listbox); mStartSelectionIndex = kNothingSelected; mEndSelectionIndex = kNothingSelected; } dom::HTMLOptionsCollection* nsListControlFrame::GetOptions() const { return Select().Options(); } dom::HTMLOptionElement* nsListControlFrame::GetOption(uint32_t aIndex) const { return Select().Item(aIndex); } NS_IMETHODIMP nsListControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) { if (aSelected) { ScrollToIndex(aIndex); } return NS_OK; } void nsListControlFrame::OnContentReset() { ResetList(true); } void nsListControlFrame::ResetList(bool aAllowScrolling) { // if all the frames aren't here // don't bother reseting if (!mIsAllFramesHere) { return; } if (aAllowScrolling) { mPostChildrenLoadedReset = true; // Scroll to the selected index int32_t indexToSelect = kNothingSelected; HTMLSelectElement* selectElement = HTMLSelectElement::FromNode(mContent); if (selectElement) { indexToSelect = selectElement->SelectedIndex(); AutoWeakFrame weakFrame(this); ScrollToIndex(indexToSelect); if (!weakFrame.IsAlive()) { return; } } } mStartSelectionIndex = kNothingSelected; mEndSelectionIndex = kNothingSelected; InvalidateFocus(); // Combobox will redisplay itself with the OnOptionSelected event } void nsListControlFrame::ElementStateChanged(ElementState aStates) { if (aStates.HasState(ElementState::FOCUS)) { InvalidateFocus(); } } void nsListControlFrame::GetOptionText(uint32_t aIndex, nsAString& aStr) { aStr.Truncate(); if (dom::HTMLOptionElement* optionElement = GetOption(aIndex)) { optionElement->GetRenderedLabel(aStr); } } int32_t nsListControlFrame::GetSelectedIndex() { dom::HTMLSelectElement* select = dom::HTMLSelectElement::FromNodeOrNull(mContent); return select->SelectedIndex(); } uint32_t nsListControlFrame::GetNumberOfOptions() { dom::HTMLOptionsCollection* options = GetOptions(); if (!options) { return 0; } return options->Length(); } //---------------------------------------------------------------------- // nsISelectControlFrame //---------------------------------------------------------------------- bool nsListControlFrame::CheckIfAllFramesHere() { // XXX Need to find a fail proof way to determine that // all the frames are there mIsAllFramesHere = true; // now make sure we have a frame each piece of content return mIsAllFramesHere; } NS_IMETHODIMP nsListControlFrame::DoneAddingChildren(bool aIsDone) { mIsAllContentHere = aIsDone; if (mIsAllContentHere) { // Here we check to see if all the frames have been created // for all the content. // If so, then we can initialize; if (!mIsAllFramesHere) { // if all the frames are now present we can initialize if (CheckIfAllFramesHere()) { mHasBeenInitialized = true; ResetList(true); } } } return NS_OK; } NS_IMETHODIMP nsListControlFrame::AddOption(int32_t aIndex) { if (!mIsAllContentHere) { mIsAllContentHere = Select().IsDoneAddingChildren(); if (!mIsAllContentHere) { mIsAllFramesHere = false; mHasBeenInitialized = false; } else { mIsAllFramesHere = (aIndex == static_cast<int32_t>(GetNumberOfOptions() - 1)); } } // Make sure we scroll to the selected option as needed mNeedToReset = true; if (!mHasBeenInitialized) { return NS_OK; } mPostChildrenLoadedReset = mIsAllContentHere; return NS_OK; } static int32_t DecrementAndClamp(int32_t aSelectionIndex, int32_t aLength) { return aLength == 0 ? nsListControlFrame::kNothingSelected : std::max(0, aSelectionIndex - 1); } NS_IMETHODIMP nsListControlFrame::RemoveOption(int32_t aIndex) { MOZ_ASSERT(aIndex >= 0, "negative <option> index"); // Need to reset if we're a dropdown if (mStartSelectionIndex != kNothingSelected) { NS_ASSERTION(mEndSelectionIndex != kNothingSelected, ""); int32_t numOptions = GetNumberOfOptions(); // NOTE: numOptions is the new number of options whereas aIndex is the // unadjusted index of the removed option (hence the <= below). NS_ASSERTION(aIndex <= numOptions, "out-of-bounds <option> index"); int32_t forward = mEndSelectionIndex - mStartSelectionIndex; int32_t* low = forward >= 0 ? &mStartSelectionIndex : &mEndSelectionIndex; int32_t* high = forward >= 0 ? &mEndSelectionIndex : &mStartSelectionIndex; if (aIndex < *low) { *low = ::DecrementAndClamp(*low, numOptions); } if (aIndex <= *high) { *high = ::DecrementAndClamp(*high, numOptions); } if (forward == 0) { *low = *high; } } else { NS_ASSERTION(mEndSelectionIndex == kNothingSelected, ""); } InvalidateFocus(); return NS_OK; } //--------------------------------------------------------- // Set the option selected in the DOM. This method is named // as it is because it indicates that the frame is the source // of this event rather than the receiver. bool nsListControlFrame::SetOptionsSelectedFromFrame(int32_t aStartIndex, int32_t aEndIndex, bool aValue, bool aClearAll) { using OptionFlag = HTMLSelectElement::OptionFlag; RefPtr<HTMLSelectElement> selectElement = HTMLSelectElement::FromNode(mContent); HTMLSelectElement::OptionFlags mask = OptionFlag::Notify; if (aValue) { mask += OptionFlag::IsSelected; } if (aClearAll) { mask += OptionFlag::ClearAll; } return selectElement->SetOptionsSelectedByIndex(aStartIndex, aEndIndex, mask); } bool nsListControlFrame::ToggleOptionSelectedFromFrame(int32_t aIndex) { RefPtr<HTMLOptionElement> option = GetOption(static_cast<uint32_t>(aIndex)); NS_ENSURE_TRUE(option, false); RefPtr<HTMLSelectElement> selectElement = HTMLSelectElement::FromNode(mContent); HTMLSelectElement::OptionFlags mask = HTMLSelectElement::OptionFlag::Notify; if (!option->Selected()) { mask += HTMLSelectElement::OptionFlag::IsSelected; } return selectElement->SetOptionsSelectedByIndex(aIndex, aIndex, mask); } // Dispatch event and such bool nsListControlFrame::UpdateSelection() { if (mIsAllFramesHere) { // if it's a combobox, display the new text. Note that after // FireOnInputAndOnChange we might be dead, as that can run script. AutoWeakFrame weakFrame(this); if (mIsAllContentHere) { RefPtr listener = mEventListener; listener->FireOnInputAndOnChange(); } return weakFrame.IsAlive(); } return true; } NS_IMETHODIMP_(void) nsListControlFrame::OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) { #ifdef ACCESSIBILITY nsCOMPtr<nsIContent> prevOption = GetCurrentOption(); #endif AutoWeakFrame weakFrame(this); ScrollToIndex(aNewIndex); if (!weakFrame.IsAlive()) { return; } mStartSelectionIndex = aNewIndex; mEndSelectionIndex = aNewIndex; InvalidateFocus(); #ifdef ACCESSIBILITY if (aOldIndex != aNewIndex) { FireMenuItemActiveEvent(prevOption); } #endif } //---------------------------------------------------------------------- // End nsISelectControlFrame //---------------------------------------------------------------------- class AsyncReset final : public Runnable { public: AsyncReset(nsListControlFrame* aFrame, bool aScroll) : Runnable("AsyncReset"), mFrame(aFrame), mScroll(aScroll) {} MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { if (mFrame.IsAlive()) { static_cast<nsListControlFrame*>(mFrame.GetFrame())->ResetList(mScroll); } return NS_OK; } private: WeakFrame mFrame; bool mScroll; }; bool nsListControlFrame::ReflowFinished() { if (mNeedToReset && !mReflowWasInterrupted) { mNeedToReset = false; // Suppress scrolling to the selected element if we restored scroll // history state AND the list contents have not changed since we loaded // all the children AND nothing else forced us to scroll by calling // ResetList(true). The latter two conditions are folded into // mPostChildrenLoadedReset. // // The idea is that we want scroll history restoration to trump ResetList // scrolling to the selected element, when the ResetList was probably only // caused by content loading normally. const bool scroll = !DidHistoryRestore() || mPostChildrenLoadedReset; nsContentUtils::AddScriptRunner(new AsyncReset(this, scroll)); } mReflowWasInterrupted = false; return ScrollContainerFrame::ReflowFinished(); } #ifdef DEBUG_FRAME_DUMP nsresult nsListControlFrame::GetFrameName(nsAString& aResult) const { return MakeFrameName(u"ListControl"_ns, aResult); } #endif nscoord nsListControlFrame::GetBSizeOfARow() { return BSizeOfARow(); } bool nsListControlFrame::IsOptionInteractivelySelectable(int32_t aIndex) const { auto& select = Select(); if (HTMLOptionElement* item = select.Item(aIndex)) { return IsOptionInteractivelySelectable(&select, item); } return false; } bool nsListControlFrame::IsOptionInteractivelySelectable( HTMLSelectElement* aSelect, HTMLOptionElement* aOption) { return !aSelect->IsOptionDisabled(aOption) && aOption->GetPrimaryFrame(); } nscoord nsListControlFrame::CalcFallbackRowBSize(float aFontSizeInflation) { RefPtr<nsFontMetrics> fontMet = nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation); return fontMet->MaxHeight(); } nscoord nsListControlFrame::CalcIntrinsicBSize(nscoord aBSizeOfARow, int32_t aNumberOfOptions) { if (Style()->StyleUIReset()->mFieldSizing == StyleFieldSizing::Content) { int32_t length = GetNumberOfRows(); return length * aBSizeOfARow; } mNumDisplayRows = Select().Size(); if (mNumDisplayRows < 1) { mNumDisplayRows = 4; } return mNumDisplayRows * aBSizeOfARow; } #ifdef ACCESSIBILITY void nsListControlFrame::FireMenuItemActiveEvent(nsIContent* aPreviousOption) { if (!IsFocused()) { return; } nsIContent* optionContent = GetCurrentOption(); if (aPreviousOption == optionContent) { // No change return; } if (aPreviousOption) { FireDOMEvent(u"DOMMenuItemInactive"_ns, aPreviousOption); } if (optionContent) { FireDOMEvent(u"DOMMenuItemActive"_ns, optionContent); } } #endif nsresult nsListControlFrame::GetIndexFromDOMEvent(dom::Event* aMouseEvent, int32_t& aCurIndex) { if (PresShell::GetCapturingContent() != mContent) { // If we're not capturing, then ignore movement in the border nsPoint pt = nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aMouseEvent, this); nsRect borderInnerEdge = GetScrollPortRect(); if (!borderInnerEdge.Contains(pt)) { return NS_ERROR_FAILURE; } } RefPtr<dom::HTMLOptionElement> option; for (nsCOMPtr<nsIContent> content = PresContext()->EventStateManager()->GetEventTargetContent(nullptr); content && !option; content = content->GetParent()) { option = dom::HTMLOptionElement::FromNode(content); } if (option) { aCurIndex = option->Index(); MOZ_ASSERT(aCurIndex >= 0); return NS_OK; } return NS_ERROR_FAILURE; } nsresult nsListControlFrame::HandleLeftButtonMouseDown( dom::Event* aMouseEvent) { int32_t selectedIndex; if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { // Handle Like List CaptureMouseEvents(true); AutoWeakFrame weakFrame(this); bool change = HandleListSelection(aMouseEvent, selectedIndex); // might destroy us if (!weakFrame.IsAlive()) { return NS_OK; } mChangesSinceDragStart = change; } return NS_OK; } nsresult nsListControlFrame::HandleLeftButtonMouseUp(dom::Event* aMouseEvent) { if (!StyleVisibility()->IsVisible()) { return NS_OK; } // Notify if (mChangesSinceDragStart) { // reset this so that future MouseUps without a prior MouseDown // won't fire onchange mChangesSinceDragStart = false; RefPtr listener = mEventListener; listener->FireOnInputAndOnChange(); // Note that `this` may be dead now, as the above call runs script. } return NS_OK; } nsresult nsListControlFrame::DragMove(dom::Event* aMouseEvent) { NS_ASSERTION(aMouseEvent, "aMouseEvent is null."); int32_t selectedIndex; if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) { // Don't waste cycles if we already dragged over this item if (selectedIndex == mEndSelectionIndex) { return NS_OK; } MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); NS_ASSERTION(mouseEvent, "aMouseEvent is not a MouseEvent!"); bool isControl; #ifdef XP_MACOSX isControl = mouseEvent->MetaKey(); #else isControl = mouseEvent->CtrlKey(); #endif AutoWeakFrame weakFrame(this); // Turn SHIFT on when you are dragging, unless control is on. bool wasChanged = PerformSelection(selectedIndex, !isControl, isControl); if (!weakFrame.IsAlive()) { return NS_OK; } mChangesSinceDragStart = mChangesSinceDragStart || wasChanged; } return NS_OK; } //---------------------------------------------------------------------- // Scroll helpers. //---------------------------------------------------------------------- void nsListControlFrame::ScrollToIndex(int32_t aIndex) { if (aIndex < 0) { // XXX shouldn't we just do nothing if we're asked to scroll to // kNothingSelected? ScrollTo(nsPoint(0, 0), ScrollMode::Instant); } else { RefPtr<dom::HTMLOptionElement> option = GetOption(AssertedCast<uint32_t>(aIndex)); if (option) { ScrollToFrame(*option); } } } void nsListControlFrame::ScrollToFrame(dom::HTMLOptionElement& aOptElement) { // otherwise we find the content's frame and scroll to it if (nsIFrame* childFrame = aOptElement.GetPrimaryFrame()) { RefPtr<mozilla::PresShell> presShell = PresShell(); presShell->ScrollFrameIntoView(childFrame, Nothing(), ScrollAxis(), ScrollAxis(), ScrollFlags::ScrollOverflowHidden | ScrollFlags::ScrollFirstAncestorOnly); } } void nsListControlFrame::UpdateSelectionAfterKeyEvent( int32_t aNewIndex, uint32_t aCharCode, bool aIsShift, bool aIsControlOrMeta, bool aIsControlSelectMode) { // If you hold control, but not shift, no key will actually do anything // except space. AutoWeakFrame weakFrame(this); bool wasChanged = false; if (aIsControlOrMeta && !aIsShift && aCharCode != ' ') { #ifdef ACCESSIBILITY nsCOMPtr<nsIContent> prevOption = GetCurrentOption(); #endif mStartSelectionIndex = aNewIndex; mEndSelectionIndex = aNewIndex; InvalidateFocus(); ScrollToIndex(aNewIndex); if (!weakFrame.IsAlive()) { return; } #ifdef ACCESSIBILITY FireMenuItemActiveEvent(prevOption); #endif } else if (aIsControlSelectMode && aCharCode == ' ') { wasChanged = SingleSelection(aNewIndex, true); } else { wasChanged = PerformSelection(aNewIndex, aIsShift, aIsControlOrMeta); } if (wasChanged && weakFrame.IsAlive()) { // dispatch event, update combobox, etc. UpdateSelection(); } }