diff options
Diffstat (limited to 'layout/forms/nsListControlFrame.cpp')
-rw-r--r-- | layout/forms/nsListControlFrame.cpp | 1211 |
1 files changed, 1211 insertions, 0 deletions
diff --git a/layout/forms/nsListControlFrame.cpp b/layout/forms/nsListControlFrame.cpp new file mode 100644 index 0000000000..3dc4acd3b3 --- /dev/null +++ b/layout/forms/nsListControlFrame.cpp @@ -0,0 +1,1211 @@ +/* -*- 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 "nsIScrollableFrame.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; + +// Static members +nsListControlFrame* nsListControlFrame::mFocused = nullptr; + +//--------------------------------------------------------- +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) + : nsHTMLScrollFrame(aStyle, aPresContext, kClassID, false), + mMightNeedSecondPass(false), + mHasPendingInterruptAtStartOfReflow(false), + mForceSelection(false) { + mChangesSinceDragStart = false; + + mIsAllContentHere = false; + mIsAllFramesHere = false; + mHasBeenInitialized = false; + mNeedToReset = true; + mPostChildrenLoadedReset = 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(); + nsHTMLScrollFrame::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"); + + nsHTMLScrollFrame::BuildDisplayList(aBuilder, aLists); +} + +HTMLOptionElement* nsListControlFrame::GetCurrentOption() const { + return mEventListener->GetCurrentOption(); +} + +/** + * 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 (mFocused != this) 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 (mFocused != this) return; + + nsIFrame* containerFrame = GetOptionsContainer(); + if (containerFrame) { + containerFrame->InvalidateFrame(); + } +} + +NS_QUERYFRAME_HEAD(nsListControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsISelectControlFrame) + NS_QUERYFRAME_ENTRY(nsListControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsHTMLScrollFrame) + +#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::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + + // Always add scrollbar inline sizes to the pref-inline-size 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(); + Maybe<nscoord> containISize = ContainIntrinsicISize(); + result = containISize ? *containISize + : GetScrolledFrame()->GetPrefISize(aRenderingContext); + LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes()); + result = NSCoordSaturatingAdd(result, scrollbarSize.IStartEnd(wm)); + return result; +} + +nscoord nsListControlFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + + // Always add scrollbar inline sizes to the min-inline-size 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(); + Maybe<nscoord> containISize = ContainIntrinsicISize(); + result = containISize ? *containISize + : GetScrolledFrame()->GetMinISize(aRenderingContext); + LogicalMargin scrollbarSize(wm, GetDesiredScrollbarSizes()); + 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"); + + SchedulePaint(); + + mHasPendingInterruptAtStartOfReflow = aPresContext->HasPendingInterrupt(); + + // 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); + } + + nsHTMLScrollFrame::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 nsHTMLScrollFrame that + // we're depending on? + nsHTMLScrollFrame::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 nsGfxScrollFrame just adds in the border.... + aStatus.Reset(); + nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); +} + +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) { + bool wasChanged = false; + + if (aClickedIndex == kNothingSelected && !mForceSelection) { + // Ignore kNothingSelected unless the selection is forced + } else if (GetMultiple()) { + 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 + } + } 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 + // nsHTMLScrollFrame if we're disabled. + if (IsContentDisabled()) { + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + + return nsHTMLScrollFrame::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 = mContent->IsDoneAddingChildren(); + if (!mIsAllContentHere) { + mIsAllFramesHere = false; + mHasBeenInitialized = false; + } + } + nsHTMLScrollFrame::SetInitialChildList(aListID, std::move(aChildList)); +} + +HTMLSelectElement& nsListControlFrame::Select() const { + return *static_cast<HTMLSelectElement*>(GetContent()); +} + +//--------------------------------------------------------- +void nsListControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsHTMLScrollFrame::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::SetFocus(bool aOn, bool aRepaint) { + InvalidateFocus(); + + if (aOn) { + mFocused = this; + } else { + mFocused = nullptr; + } + + 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) { +#ifdef DO_REFLOW_DEBUG + printf("---- Id: %d nsLCF %p Added Option %d\n", mReflowId, this, aIndex); +#endif + + if (!mIsAllContentHere) { + mIsAllContentHere = mContent->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 (mForceSelection) { + mask += OptionFlag::SetDisabled; + } + 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; +}; + +nsresult nsListControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + if (nsGkAtoms::selected == aName) { + return NS_ERROR_INVALID_ARG; // Selected is readonly according to spec. + } else if (nsGkAtoms::selectedindex == aName) { + // You shouldn't be calling me for this!!! + return NS_ERROR_INVALID_ARG; + } + + // We should be told about selectedIndex by the DOM element through + // OnOptionSelected + + return NS_OK; +} + +void nsListControlFrame::DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput) { + bool wasInterrupted = !mHasPendingInterruptAtStartOfReflow && + aPresContext->HasPendingInterrupt(); + + nsHTMLScrollFrame::DidReflow(aPresContext, aReflowInput); + + if (mNeedToReset && !wasInterrupted) { + 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)); + } + + mHasPendingInterruptAtStartOfReflow = false; +} + +#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) { + mNumDisplayRows = Select().Size(); + if (mNumDisplayRows < 1) { + mNumDisplayRows = 4; + } + return mNumDisplayRows * aBSizeOfARow; +} + +#ifdef ACCESSIBILITY +void nsListControlFrame::FireMenuItemActiveEvent(nsIContent* aPreviousOption) { + if (mFocused != this) { + 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(); + } +} |