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 "nscore.h"
8 #include "nsCOMPtr.h"
9 #include "nsUnicharUtils.h"
10 #include "nsListControlFrame.h"
11 #include "nsCheckboxRadioFrame.h"  // for COMPARE macro
12 #include "nsGkAtoms.h"
13 #include "nsComboboxControlFrame.h"
14 #include "nsFontMetrics.h"
15 #include "nsIScrollableFrame.h"
16 #include "nsCSSRendering.h"
17 #include "nsIDOMEventListener.h"
18 #include "nsLayoutUtils.h"
19 #include "nsDisplayList.h"
20 #include "nsContentUtils.h"
21 #include "mozilla/Attributes.h"
22 #include "mozilla/dom/Event.h"
23 #include "mozilla/dom/HTMLOptGroupElement.h"
24 #include "mozilla/dom/HTMLOptionsCollection.h"
25 #include "mozilla/dom/HTMLSelectElement.h"
26 #include "mozilla/dom/MouseEvent.h"
27 #include "mozilla/dom/MouseEventBinding.h"
28 #include "mozilla/EventStateManager.h"
29 #include "mozilla/EventStates.h"
30 #include "mozilla/LookAndFeel.h"
31 #include "mozilla/MouseEvents.h"
32 #include "mozilla/Preferences.h"
33 #include "mozilla/PresShell.h"
34 #include "mozilla/StaticPrefs_browser.h"
35 #include "mozilla/StaticPrefs_ui.h"
36 #include "mozilla/TextEvents.h"
37 #include <algorithm>
38 
39 using namespace mozilla;
40 using namespace mozilla::dom;
41 
42 // Constants
43 const uint32_t kMaxDropDownRows = 20;  // matches the setting for 4.x browsers
44 const int32_t kNothingSelected = -1;
45 
46 // Static members
47 nsListControlFrame* nsListControlFrame::mFocused = nullptr;
48 nsString* nsListControlFrame::sIncrementalString = nullptr;
49 
50 DOMTimeStamp nsListControlFrame::gLastKeyTime = 0;
51 
52 /******************************************************************************
53  * nsListEventListener
54  * This class is responsible for propagating events to the nsListControlFrame.
55  * Frames are not refcounted so they can't be used as event listeners.
56  *****************************************************************************/
57 
58 class nsListEventListener final : public nsIDOMEventListener {
59  public:
nsListEventListener(nsListControlFrame * aFrame)60   explicit nsListEventListener(nsListControlFrame* aFrame) : mFrame(aFrame) {}
61 
SetFrame(nsListControlFrame * aFrame)62   void SetFrame(nsListControlFrame* aFrame) { mFrame = aFrame; }
63 
64   NS_DECL_ISUPPORTS
65 
66   // nsIDOMEventListener
67   MOZ_CAN_RUN_SCRIPT_BOUNDARY
68   NS_IMETHOD HandleEvent(Event* aEvent) override;
69 
70  private:
71   ~nsListEventListener() = default;
72 
73   nsListControlFrame* mFrame;
74 };
75 
76 //---------------------------------------------------------
NS_NewListControlFrame(PresShell * aPresShell,ComputedStyle * aStyle)77 nsContainerFrame* NS_NewListControlFrame(PresShell* aPresShell,
78                                          ComputedStyle* aStyle) {
79   nsListControlFrame* it =
80       new (aPresShell) nsListControlFrame(aStyle, aPresShell->GetPresContext());
81 
82   it->AddStateBits(NS_FRAME_INDEPENDENT_SELECTION);
83 
84   return it;
85 }
86 
NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame)87 NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame)
88 
89 //---------------------------------------------------------
90 nsListControlFrame::nsListControlFrame(ComputedStyle* aStyle,
91                                        nsPresContext* aPresContext)
92     : nsHTMLScrollFrame(aStyle, aPresContext, kClassID, false),
93       mView(nullptr),
94       mMightNeedSecondPass(false),
95       mHasPendingInterruptAtStartOfReflow(false),
96       mDropdownCanGrow(false),
97       mForceSelection(false),
98       mLastDropdownComputedBSize(NS_UNCONSTRAINEDSIZE) {
99   mComboboxFrame = nullptr;
100   mChangesSinceDragStart = false;
101   mButtonDown = false;
102 
103   mIsAllContentHere = false;
104   mIsAllFramesHere = false;
105   mHasBeenInitialized = false;
106   mNeedToReset = true;
107   mPostChildrenLoadedReset = false;
108 
109   mControlSelectMode = false;
110 }
111 
112 //---------------------------------------------------------
~nsListControlFrame()113 nsListControlFrame::~nsListControlFrame() { mComboboxFrame = nullptr; }
114 
ShouldFireDropDownEvent()115 static bool ShouldFireDropDownEvent() {
116   return (XRE_IsContentProcess() &&
117           StaticPrefs::browser_tabs_remote_desktopbehavior()) ||
118          Preferences::GetBool("dom.select_popup_in_parent.enabled", false);
119 }
120 
121 // for Bug 47302 (remove this comment later)
DestroyFrom(nsIFrame * aDestructRoot,PostDestroyData & aPostDestroyData)122 void nsListControlFrame::DestroyFrom(nsIFrame* aDestructRoot,
123                                      PostDestroyData& aPostDestroyData) {
124   // get the receiver interface from the browser button's content node
125   NS_ENSURE_TRUE_VOID(mContent);
126 
127   // Clear the frame pointer on our event listener, just in case the
128   // event listener can outlive the frame.
129 
130   mEventListener->SetFrame(nullptr);
131 
132   mContent->RemoveSystemEventListener(NS_LITERAL_STRING("keydown"),
133                                       mEventListener, false);
134   mContent->RemoveSystemEventListener(NS_LITERAL_STRING("keypress"),
135                                       mEventListener, false);
136   mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mousedown"),
137                                       mEventListener, false);
138   mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mouseup"),
139                                       mEventListener, false);
140   mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mousemove"),
141                                       mEventListener, false);
142 
143   if (ShouldFireDropDownEvent()) {
144     nsContentUtils::AddScriptRunner(
145         new AsyncEventDispatcher(mContent, NS_LITERAL_STRING("mozhidedropdown"),
146                                  CanBubble::eYes, ChromeOnlyDispatch::eYes));
147   }
148 
149   nsCheckboxRadioFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false);
150   nsHTMLScrollFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
151 }
152 
BuildDisplayList(nsDisplayListBuilder * aBuilder,const nsDisplayListSet & aLists)153 void nsListControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
154                                           const nsDisplayListSet& aLists) {
155   // We allow visibility:hidden <select>s to contain visible options.
156 
157   // Don't allow painting of list controls when painting is suppressed.
158   // XXX why do we need this here? we should never reach this. Maybe
159   // because these can have widgets? Hmm
160   if (aBuilder->IsBackgroundOnly()) return;
161 
162   DO_GLOBAL_REFLOW_COUNT_DSP("nsListControlFrame");
163 
164   if (IsInDropDownMode()) {
165     NS_ASSERTION(NS_GET_A(mLastDropdownBackstopColor) == 255,
166                  "need an opaque backstop color");
167     // XXX Because we have an opaque widget and we get called to paint with
168     // this frame as the root of a stacking context we need make sure to draw
169     // some opaque color over the whole widget. (Bug 511323)
170     aLists.BorderBackground()->AppendNewToBottom<nsDisplaySolidColor>(
171         aBuilder, this, nsRect(aBuilder->ToReferenceFrame(this), GetSize()),
172         mLastDropdownBackstopColor);
173   }
174 
175   nsHTMLScrollFrame::BuildDisplayList(aBuilder, aLists);
176 }
177 
178 /**
179  * This is called by the SelectsAreaFrame, which is the same
180  * as the frame returned by GetOptionsContainer. It's the frame which is
181  * scrolled by us.
182  * @param aPt the offset of this frame, relative to the rendering reference
183  * frame
184  */
PaintFocus(DrawTarget * aDrawTarget,nsPoint aPt)185 void nsListControlFrame::PaintFocus(DrawTarget* aDrawTarget, nsPoint aPt) {
186   if (mFocused != this) return;
187 
188   nsPresContext* presContext = PresContext();
189 
190   nsIFrame* containerFrame = GetOptionsContainer();
191   if (!containerFrame) return;
192 
193   nsIFrame* childframe = nullptr;
194   nsCOMPtr<nsIContent> focusedContent = GetCurrentOption();
195   if (focusedContent) {
196     childframe = focusedContent->GetPrimaryFrame();
197   }
198 
199   nsRect fRect;
200   if (childframe) {
201     // get the child rect
202     fRect = childframe->GetRect();
203     // get it into our coordinates
204     fRect.MoveBy(childframe->GetParent()->GetOffsetTo(this));
205   } else {
206     float inflation = nsLayoutUtils::FontSizeInflationFor(this);
207     fRect.x = fRect.y = 0;
208     if (GetWritingMode().IsVertical()) {
209       fRect.width = GetScrollPortRect().width;
210       fRect.height = CalcFallbackRowBSize(inflation);
211     } else {
212       fRect.width = CalcFallbackRowBSize(inflation);
213       fRect.height = GetScrollPortRect().height;
214     }
215     fRect.MoveBy(containerFrame->GetOffsetTo(this));
216   }
217   fRect += aPt;
218 
219   bool lastItemIsSelected = false;
220   HTMLOptionElement* domOpt = HTMLOptionElement::FromNodeOrNull(focusedContent);
221   if (domOpt) {
222     lastItemIsSelected = domOpt->Selected();
223   }
224 
225   // set up back stop colors and then ask L&F service for the real colors
226   nscolor color = LookAndFeel::GetColor(
227       lastItemIsSelected ? LookAndFeel::ColorID::WidgetSelectForeground
228                          : LookAndFeel::ColorID::WidgetSelectBackground);
229 
230   nsCSSRendering::PaintFocus(presContext, aDrawTarget, fRect, color);
231 }
232 
InvalidateFocus()233 void nsListControlFrame::InvalidateFocus() {
234   if (mFocused != this) return;
235 
236   nsIFrame* containerFrame = GetOptionsContainer();
237   if (containerFrame) {
238     containerFrame->InvalidateFrame();
239   }
240 }
241 
242 NS_QUERYFRAME_HEAD(nsListControlFrame)
NS_QUERYFRAME_ENTRY(nsIFormControlFrame)243   NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
244   NS_QUERYFRAME_ENTRY(nsISelectControlFrame)
245   NS_QUERYFRAME_ENTRY(nsListControlFrame)
246 NS_QUERYFRAME_TAIL_INHERITING(nsHTMLScrollFrame)
247 
248 #ifdef ACCESSIBILITY
249 a11y::AccType nsListControlFrame::AccessibleType() {
250   return a11y::eHTMLSelectListType;
251 }
252 #endif
253 
254 // Return true if we found at least one <option> or non-empty <optgroup> label
255 // that has a frame.  aResult will be the maximum BSize of those.
GetMaxRowBSize(nsIFrame * aContainer,WritingMode aWM,nscoord * aResult)256 static bool GetMaxRowBSize(nsIFrame* aContainer, WritingMode aWM,
257                            nscoord* aResult) {
258   bool found = false;
259   for (nsIFrame* child : aContainer->PrincipalChildList()) {
260     if (child->GetContent()->IsHTMLElement(nsGkAtoms::optgroup)) {
261       // An optgroup; drill through any scroll frame and recurse.  |inner| might
262       // be null here though if |inner| is an anonymous leaf frame of some sort.
263       auto inner = child->GetContentInsertionFrame();
264       if (inner && GetMaxRowBSize(inner, aWM, aResult)) {
265         found = true;
266       }
267     } else {
268       // an option or optgroup label
269       bool isOptGroupLabel =
270           child->Style()->IsPseudoElement() &&
271           aContainer->GetContent()->IsHTMLElement(nsGkAtoms::optgroup);
272       nscoord childBSize = child->BSize(aWM);
273       // XXX bug 1499176: skip empty <optgroup> labels (zero bsize) for now
274       if (!isOptGroupLabel || childBSize > nscoord(0)) {
275         found = true;
276         *aResult = std::max(childBSize, *aResult);
277       }
278     }
279   }
280   return found;
281 }
282 
283 //-----------------------------------------------------------------
284 // Main Reflow for ListBox/Dropdown
285 //-----------------------------------------------------------------
286 
CalcBSizeOfARow()287 nscoord nsListControlFrame::CalcBSizeOfARow() {
288   // Calculate the block size in our writing mode of a single row in the
289   // listbox or dropdown list by using the tallest thing in the subtree,
290   // since there may be option groups in addition to option elements,
291   // either of which may be visible or invisible, may use different
292   // fonts, etc.
293   nscoord rowBSize(0);
294   if (StyleDisplay()->IsContainSize() ||
295       !GetMaxRowBSize(GetOptionsContainer(), GetWritingMode(), &rowBSize)) {
296     // We don't have any <option>s or <optgroup> labels with a frame.
297     // (Or we're size-contained, which has the same outcome for our sizing.)
298     float inflation = nsLayoutUtils::FontSizeInflationFor(this);
299     rowBSize = CalcFallbackRowBSize(inflation);
300   }
301   return rowBSize;
302 }
303 
GetPrefISize(gfxContext * aRenderingContext)304 nscoord nsListControlFrame::GetPrefISize(gfxContext* aRenderingContext) {
305   nscoord result;
306   DISPLAY_PREF_INLINE_SIZE(this, result);
307 
308   // Always add scrollbar inline sizes to the pref-inline-size of the
309   // scrolled content. Combobox frames depend on this happening in the
310   // dropdown, and standalone listboxes are overflow:scroll so they need
311   // it too.
312   WritingMode wm = GetWritingMode();
313   result = StyleDisplay()->IsContainSize()
314                ? 0
315                : GetScrolledFrame()->GetPrefISize(aRenderingContext);
316   LogicalMargin scrollbarSize(
317       wm, GetDesiredScrollbarSizes(PresContext(), aRenderingContext));
318   result = NSCoordSaturatingAdd(result, scrollbarSize.IStartEnd(wm));
319   return result;
320 }
321 
GetMinISize(gfxContext * aRenderingContext)322 nscoord nsListControlFrame::GetMinISize(gfxContext* aRenderingContext) {
323   nscoord result;
324   DISPLAY_MIN_INLINE_SIZE(this, result);
325 
326   // Always add scrollbar inline sizes to the min-inline-size of the
327   // scrolled content. Combobox frames depend on this happening in the
328   // dropdown, and standalone listboxes are overflow:scroll so they need
329   // it too.
330   WritingMode wm = GetWritingMode();
331   result = StyleDisplay()->IsContainSize()
332                ? 0
333                : GetScrolledFrame()->GetMinISize(aRenderingContext);
334   LogicalMargin scrollbarSize(
335       wm, GetDesiredScrollbarSizes(PresContext(), aRenderingContext));
336   result += scrollbarSize.IStartEnd(wm);
337 
338   return result;
339 }
340 
Reflow(nsPresContext * aPresContext,ReflowOutput & aDesiredSize,const ReflowInput & aReflowInput,nsReflowStatus & aStatus)341 void nsListControlFrame::Reflow(nsPresContext* aPresContext,
342                                 ReflowOutput& aDesiredSize,
343                                 const ReflowInput& aReflowInput,
344                                 nsReflowStatus& aStatus) {
345   MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
346   NS_WARNING_ASSERTION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE,
347                        "Must have a computed inline size");
348 
349   SchedulePaint();
350 
351   mHasPendingInterruptAtStartOfReflow = aPresContext->HasPendingInterrupt();
352 
353   // If all the content and frames are here
354   // then initialize it before reflow
355   if (mIsAllContentHere && !mHasBeenInitialized) {
356     if (false == mIsAllFramesHere) {
357       CheckIfAllFramesHere();
358     }
359     if (mIsAllFramesHere && !mHasBeenInitialized) {
360       mHasBeenInitialized = true;
361     }
362   }
363 
364   if (GetStateBits() & NS_FRAME_FIRST_REFLOW) {
365     nsCheckboxRadioFrame::RegUnRegAccessKey(this, true);
366   }
367 
368   if (IsInDropDownMode()) {
369     ReflowAsDropdown(aPresContext, aDesiredSize, aReflowInput, aStatus);
370     return;
371   }
372 
373   MarkInReflow();
374   /*
375    * Due to the fact that our intrinsic block size depends on the block
376    * sizes of our kids, we end up having to do two-pass reflow, in
377    * general -- the first pass to find the intrinsic block size and a
378    * second pass to reflow the scrollframe at that block size (which
379    * will size the scrollbars correctly, etc).
380    *
381    * Naturally, we want to avoid doing the second reflow as much as
382    * possible.
383    * We can skip it in the following cases (in all of which the first
384    * reflow is already happening at the right block size):
385    *
386    * - We're reflowing with a constrained computed block size -- just
387    *   use that block size.
388    * - We're not dirty and have no dirty kids and shouldn't be reflowing
389    *   all kids.  In this case, our cached max block size of a child is
390    *   not going to change.
391    * - We do our first reflow using our cached max block size of a
392    *   child, then compute the new max block size and it's the same as
393    *   the old one.
394    */
395 
396   bool autoBSize = (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE);
397 
398   mMightNeedSecondPass = autoBSize && (NS_SUBTREE_DIRTY(this) ||
399                                        aReflowInput.ShouldReflowAllKids());
400 
401   ReflowInput state(aReflowInput);
402   int32_t length = GetNumberOfRows();
403 
404   nscoord oldBSizeOfARow = BSizeOfARow();
405 
406   if (!(GetStateBits() & NS_FRAME_FIRST_REFLOW) && autoBSize) {
407     // When not doing an initial reflow, and when the block size is
408     // auto, start off with our computed block size set to what we'd
409     // expect our block size to be.
410     nscoord computedBSize = CalcIntrinsicBSize(oldBSizeOfARow, length);
411     computedBSize = state.ApplyMinMaxBSize(computedBSize);
412     state.SetComputedBSize(computedBSize);
413   }
414 
415   nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
416 
417   if (!mMightNeedSecondPass) {
418     NS_ASSERTION(!autoBSize || BSizeOfARow() == oldBSizeOfARow,
419                  "How did our BSize of a row change if nothing was dirty?");
420     NS_ASSERTION(!autoBSize || !(GetStateBits() & NS_FRAME_FIRST_REFLOW),
421                  "How do we not need a second pass during initial reflow at "
422                  "auto BSize?");
423     NS_ASSERTION(!IsScrollbarUpdateSuppressed(),
424                  "Shouldn't be suppressing if we don't need a second pass!");
425     if (!autoBSize) {
426       // Update our mNumDisplayRows based on our new row block size now
427       // that we know it.  Note that if autoBSize and we landed in this
428       // code then we already set mNumDisplayRows in CalcIntrinsicBSize.
429       //  Also note that we can't use BSizeOfARow() here because that
430       // just uses a cached value that we didn't compute.
431       nscoord rowBSize = CalcBSizeOfARow();
432       if (rowBSize == 0) {
433         // Just pick something
434         mNumDisplayRows = 1;
435       } else {
436         mNumDisplayRows = std::max(1, state.ComputedBSize() / rowBSize);
437       }
438     }
439 
440     return;
441   }
442 
443   mMightNeedSecondPass = false;
444 
445   // Now see whether we need a second pass.  If we do, our
446   // nsSelectsAreaFrame will have suppressed the scrollbar update.
447   if (!IsScrollbarUpdateSuppressed()) {
448     // All done.  No need to do more reflow.
449     return;
450   }
451 
452   SetSuppressScrollbarUpdate(false);
453 
454   // Gotta reflow again.
455   // XXXbz We're just changing the block size here; do we need to dirty
456   // ourselves or anything like that?  We might need to, per the letter
457   // of the reflow protocol, but things seem to work fine without it...
458   // Is that just an implementation detail of nsHTMLScrollFrame that
459   // we're depending on?
460   nsHTMLScrollFrame::DidReflow(aPresContext, &state);
461 
462   // Now compute the block size we want to have
463   nscoord computedBSize = CalcIntrinsicBSize(BSizeOfARow(), length);
464   computedBSize = state.ApplyMinMaxBSize(computedBSize);
465   state.SetComputedBSize(computedBSize);
466 
467   // XXXbz to make the ascent really correct, we should add our
468   // mComputedPadding.top to it (and subtract it from descent).  Need that
469   // because nsGfxScrollFrame just adds in the border....
470   aStatus.Reset();
471   nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
472 }
473 
ReflowAsDropdown(nsPresContext * aPresContext,ReflowOutput & aDesiredSize,const ReflowInput & aReflowInput,nsReflowStatus & aStatus)474 void nsListControlFrame::ReflowAsDropdown(nsPresContext* aPresContext,
475                                           ReflowOutput& aDesiredSize,
476                                           const ReflowInput& aReflowInput,
477                                           nsReflowStatus& aStatus) {
478   MOZ_ASSERT(aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE,
479              "We should not have a computed block size here!");
480 
481   mMightNeedSecondPass =
482       NS_SUBTREE_DIRTY(this) || aReflowInput.ShouldReflowAllKids();
483 
484   WritingMode wm = aReflowInput.GetWritingMode();
485 #ifdef DEBUG
486   nscoord oldBSizeOfARow = BSizeOfARow();
487   nscoord oldVisibleBSize = (GetStateBits() & NS_FRAME_FIRST_REFLOW)
488                                 ? NS_UNCONSTRAINEDSIZE
489                                 : GetScrolledFrame()->BSize(wm);
490 #endif
491 
492   ReflowInput state(aReflowInput);
493 
494   if (!(GetStateBits() & NS_FRAME_FIRST_REFLOW)) {
495     // When not doing an initial reflow, and when the block size is
496     // auto, start off with our computed block size set to what we'd
497     // expect our block size to be.
498     // Note: At this point, mLastDropdownComputedBSize can be
499     // NS_UNCONSTRAINEDSIZE in cases when last time we didn't have to
500     // constrain the block size.  That's fine; just do the same thing as
501     // last time.
502     state.SetComputedBSize(mLastDropdownComputedBSize);
503   }
504 
505   nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
506 
507   if (!mMightNeedSecondPass) {
508     NS_ASSERTION(oldVisibleBSize == GetScrolledFrame()->BSize(wm),
509                  "How did our kid's BSize change if nothing was dirty?");
510     NS_ASSERTION(BSizeOfARow() == oldBSizeOfARow,
511                  "How did our BSize of a row change if nothing was dirty?");
512     NS_ASSERTION(!IsScrollbarUpdateSuppressed(),
513                  "Shouldn't be suppressing if we don't need a second pass!");
514     NS_ASSERTION(!(GetStateBits() & NS_FRAME_FIRST_REFLOW),
515                  "How can we avoid a second pass during first reflow?");
516     return;
517   }
518 
519   mMightNeedSecondPass = false;
520 
521   // Now see whether we need a second pass.  If we do, our nsSelectsAreaFrame
522   // will have suppressed the scrollbar update.
523   if (!IsScrollbarUpdateSuppressed()) {
524     // All done.  No need to do more reflow.
525     NS_ASSERTION(!(GetStateBits() & NS_FRAME_FIRST_REFLOW),
526                  "How can we avoid a second pass during first reflow?");
527     return;
528   }
529 
530   SetSuppressScrollbarUpdate(false);
531 
532   nscoord visibleBSize = GetScrolledFrame()->BSize(wm);
533   nscoord blockSizeOfARow = BSizeOfARow();
534 
535   // Gotta reflow again.
536   // XXXbz We're just changing the block size here; do we need to dirty
537   // ourselves or anything like that?  We might need to, per the letter
538   // of the reflow protocol, but things seem to work fine without it...
539   // Is that just an implementation detail of nsHTMLScrollFrame that
540   // we're depending on?
541   nsHTMLScrollFrame::DidReflow(aPresContext, &state);
542 
543   // Now compute the block size we want to have.
544   // Note: no need to apply min/max constraints, since we have no such
545   // rules applied to the combobox dropdown.
546 
547   mDropdownCanGrow = false;
548   if (visibleBSize <= 0 || blockSizeOfARow <= 0 || XRE_IsContentProcess()) {
549     // Looks like we have no options.  Just size us to a single row
550     // block size.
551     state.SetComputedBSize(blockSizeOfARow);
552     // mNumDisplayRows is used as the number of options to move for the page
553     // up/down keys. If we're in a content process, we can't calculate
554     // mNumDisplayRows properly, but the maximum number of rows is a lot more
555     // uesful for page up/down than 1.
556     mNumDisplayRows = XRE_IsContentProcess() ? kMaxDropDownRows : 1;
557   } else {
558     nsComboboxControlFrame* combobox =
559         static_cast<nsComboboxControlFrame*>(mComboboxFrame);
560     LogicalPoint translation(wm);
561     nscoord before, after;
562     combobox->GetAvailableDropdownSpace(wm, &before, &after, &translation);
563     if (before <= 0 && after <= 0) {
564       state.SetComputedBSize(blockSizeOfARow);
565       mNumDisplayRows = 1;
566       mDropdownCanGrow = GetNumberOfRows() > 1;
567     } else {
568       nscoord bp = aReflowInput.ComputedLogicalBorderPadding().BStartEnd(wm);
569       nscoord availableBSize = std::max(before, after) - bp;
570       nscoord newBSize;
571       uint32_t rows;
572       if (visibleBSize <= availableBSize) {
573         // The dropdown fits in the available block size.
574         rows = GetNumberOfRows();
575         mNumDisplayRows = clamped<uint32_t>(rows, 1, kMaxDropDownRows);
576         if (mNumDisplayRows == rows) {
577           newBSize = visibleBSize;  // use the exact block size
578         } else {
579           newBSize = mNumDisplayRows * blockSizeOfARow;  // approximate
580           // The approximation here might actually be too big (bug 1208978);
581           // don't let it exceed the actual block-size of the list.
582           newBSize = std::min(newBSize, visibleBSize);
583         }
584       } else {
585         rows = availableBSize / blockSizeOfARow;
586         mNumDisplayRows = clamped<uint32_t>(rows, 1, kMaxDropDownRows);
587         newBSize = mNumDisplayRows * blockSizeOfARow;  // approximate
588       }
589       state.SetComputedBSize(newBSize);
590       mDropdownCanGrow = visibleBSize - newBSize >= blockSizeOfARow &&
591                          mNumDisplayRows != kMaxDropDownRows;
592     }
593   }
594 
595   mLastDropdownComputedBSize = state.ComputedBSize();
596 
597   aStatus.Reset();
598   nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
599 }
600 
GetScrollStyles() const601 ScrollStyles nsListControlFrame::GetScrollStyles() const {
602   // We can't express this in the style system yet; when we can, this can go
603   // away and GetScrollStyles can be devirtualized
604   auto style = IsInDropDownMode() ? StyleOverflow::Auto : StyleOverflow::Scroll;
605   if (GetWritingMode().IsVertical()) {
606     return ScrollStyles(style, StyleOverflow::Hidden);
607   } else {
608     return ScrollStyles(StyleOverflow::Hidden, style);
609   }
610 }
611 
ShouldPropagateComputedBSizeToScrolledContent() const612 bool nsListControlFrame::ShouldPropagateComputedBSizeToScrolledContent() const {
613   return !IsInDropDownMode();
614 }
615 
616 //---------------------------------------------------------
GetContentInsertionFrame()617 nsContainerFrame* nsListControlFrame::GetContentInsertionFrame() {
618   return GetOptionsContainer()->GetContentInsertionFrame();
619 }
620 
621 //---------------------------------------------------------
ExtendedSelection(int32_t aStartIndex,int32_t aEndIndex,bool aClearAll)622 bool nsListControlFrame::ExtendedSelection(int32_t aStartIndex,
623                                            int32_t aEndIndex, bool aClearAll) {
624   return SetOptionsSelectedFromFrame(aStartIndex, aEndIndex, true, aClearAll);
625 }
626 
627 //---------------------------------------------------------
SingleSelection(int32_t aClickedIndex,bool aDoToggle)628 bool nsListControlFrame::SingleSelection(int32_t aClickedIndex,
629                                          bool aDoToggle) {
630   if (mComboboxFrame) {
631     mComboboxFrame->UpdateRecentIndex(GetSelectedIndex());
632   }
633 
634   bool wasChanged = false;
635   // Get Current selection
636   if (aDoToggle) {
637     wasChanged = ToggleOptionSelectedFromFrame(aClickedIndex);
638   } else {
639     wasChanged =
640         SetOptionsSelectedFromFrame(aClickedIndex, aClickedIndex, true, true);
641   }
642   AutoWeakFrame weakFrame(this);
643   ScrollToIndex(aClickedIndex);
644   if (!weakFrame.IsAlive()) {
645     return wasChanged;
646   }
647 
648 #ifdef ACCESSIBILITY
649   bool isCurrentOptionChanged = mEndSelectionIndex != aClickedIndex;
650 #endif
651   mStartSelectionIndex = aClickedIndex;
652   mEndSelectionIndex = aClickedIndex;
653   InvalidateFocus();
654 
655 #ifdef ACCESSIBILITY
656   if (isCurrentOptionChanged) {
657     FireMenuItemActiveEvent();
658   }
659 #endif
660 
661   return wasChanged;
662 }
663 
InitSelectionRange(int32_t aClickedIndex)664 void nsListControlFrame::InitSelectionRange(int32_t aClickedIndex) {
665   //
666   // If nothing is selected, set the start selection depending on where
667   // the user clicked and what the initial selection is:
668   // - if the user clicked *before* selectedIndex, set the start index to
669   //   the end of the first contiguous selection.
670   // - if the user clicked *after* the end of the first contiguous
671   //   selection, set the start index to selectedIndex.
672   // - if the user clicked *within* the first contiguous selection, set the
673   //   start index to selectedIndex.
674   // The last two rules, of course, boil down to the same thing: if the user
675   // clicked >= selectedIndex, return selectedIndex.
676   //
677   // This makes it so that shift click works properly when you first click
678   // in a multiple select.
679   //
680   int32_t selectedIndex = GetSelectedIndex();
681   if (selectedIndex >= 0) {
682     // Get the end of the contiguous selection
683     RefPtr<dom::HTMLOptionsCollection> options = GetOptions();
684     NS_ASSERTION(options, "Collection of options is null!");
685     uint32_t numOptions = options->Length();
686     // Push i to one past the last selected index in the group.
687     uint32_t i;
688     for (i = selectedIndex + 1; i < numOptions; i++) {
689       if (!options->ItemAsOption(i)->Selected()) {
690         break;
691       }
692     }
693 
694     if (aClickedIndex < selectedIndex) {
695       // User clicked before selection, so start selection at end of
696       // contiguous selection
697       mStartSelectionIndex = i - 1;
698       mEndSelectionIndex = selectedIndex;
699     } else {
700       // User clicked after selection, so start selection at start of
701       // contiguous selection
702       mStartSelectionIndex = selectedIndex;
703       mEndSelectionIndex = i - 1;
704     }
705   }
706 }
707 
CountOptionsAndOptgroups(nsIFrame * aFrame)708 static uint32_t CountOptionsAndOptgroups(nsIFrame* aFrame) {
709   uint32_t count = 0;
710   nsFrameList::Enumerator e(aFrame->PrincipalChildList());
711   for (; !e.AtEnd(); e.Next()) {
712     nsIFrame* child = e.get();
713     nsIContent* content = child->GetContent();
714     if (content) {
715       if (content->IsHTMLElement(nsGkAtoms::option)) {
716         ++count;
717       } else {
718         RefPtr<HTMLOptGroupElement> optgroup =
719             HTMLOptGroupElement::FromNode(content);
720         if (optgroup) {
721           nsAutoString label;
722           optgroup->GetLabel(label);
723           if (label.Length() > 0) {
724             ++count;
725           }
726           count += CountOptionsAndOptgroups(child);
727         }
728       }
729     }
730   }
731   return count;
732 }
733 
GetNumberOfRows()734 uint32_t nsListControlFrame::GetNumberOfRows() {
735   return ::CountOptionsAndOptgroups(GetContentInsertionFrame());
736 }
737 
738 //---------------------------------------------------------
PerformSelection(int32_t aClickedIndex,bool aIsShift,bool aIsControl)739 bool nsListControlFrame::PerformSelection(int32_t aClickedIndex, bool aIsShift,
740                                           bool aIsControl) {
741   bool wasChanged = false;
742 
743   if (aClickedIndex == kNothingSelected && !mForceSelection) {
744     // Ignore kNothingSelected unless the selection is forced
745   } else if (GetMultiple()) {
746     if (aIsShift) {
747       // Make sure shift+click actually does something expected when
748       // the user has never clicked on the select
749       if (mStartSelectionIndex == kNothingSelected) {
750         InitSelectionRange(aClickedIndex);
751       }
752 
753       // Get the range from beginning (low) to end (high)
754       // Shift *always* works, even if the current option is disabled
755       int32_t startIndex;
756       int32_t endIndex;
757       if (mStartSelectionIndex == kNothingSelected) {
758         startIndex = aClickedIndex;
759         endIndex = aClickedIndex;
760       } else if (mStartSelectionIndex <= aClickedIndex) {
761         startIndex = mStartSelectionIndex;
762         endIndex = aClickedIndex;
763       } else {
764         startIndex = aClickedIndex;
765         endIndex = mStartSelectionIndex;
766       }
767 
768       // Clear only if control was not pressed
769       wasChanged = ExtendedSelection(startIndex, endIndex, !aIsControl);
770       AutoWeakFrame weakFrame(this);
771       ScrollToIndex(aClickedIndex);
772       if (!weakFrame.IsAlive()) {
773         return wasChanged;
774       }
775 
776       if (mStartSelectionIndex == kNothingSelected) {
777         mStartSelectionIndex = aClickedIndex;
778       }
779 #ifdef ACCESSIBILITY
780       bool isCurrentOptionChanged = mEndSelectionIndex != aClickedIndex;
781 #endif
782       mEndSelectionIndex = aClickedIndex;
783       InvalidateFocus();
784 
785 #ifdef ACCESSIBILITY
786       if (isCurrentOptionChanged) {
787         FireMenuItemActiveEvent();
788       }
789 #endif
790     } else if (aIsControl) {
791       wasChanged = SingleSelection(aClickedIndex, true);  // might destroy us
792     } else {
793       wasChanged = SingleSelection(aClickedIndex, false);  // might destroy us
794     }
795   } else {
796     wasChanged = SingleSelection(aClickedIndex, false);  // might destroy us
797   }
798 
799   return wasChanged;
800 }
801 
802 //---------------------------------------------------------
HandleListSelection(dom::Event * aEvent,int32_t aClickedIndex)803 bool nsListControlFrame::HandleListSelection(dom::Event* aEvent,
804                                              int32_t aClickedIndex) {
805   MouseEvent* mouseEvent = aEvent->AsMouseEvent();
806   bool isControl;
807 #ifdef XP_MACOSX
808   isControl = mouseEvent->MetaKey();
809 #else
810   isControl = mouseEvent->CtrlKey();
811 #endif
812   bool isShift = mouseEvent->ShiftKey();
813   return PerformSelection(aClickedIndex, isShift,
814                           isControl);  // might destroy us
815 }
816 
817 //---------------------------------------------------------
CaptureMouseEvents(bool aGrabMouseEvents)818 void nsListControlFrame::CaptureMouseEvents(bool aGrabMouseEvents) {
819   // Currently cocoa widgets use a native popup widget which tracks clicks
820   // synchronously, so we never want to do mouse capturing. Note that we only
821   // bail if the list is in drop-down mode, and the caller is requesting capture
822   // (we let release capture requests go through to ensure that we can release
823   // capture requested via other code paths, if any exist).
824   if (aGrabMouseEvents && IsInDropDownMode() &&
825       nsComboboxControlFrame::ToolkitHasNativePopup())
826     return;
827 
828   if (aGrabMouseEvents) {
829     PresShell::SetCapturingContent(mContent, CaptureFlags::IgnoreAllowedState);
830   } else {
831     nsIContent* capturingContent = PresShell::GetCapturingContent();
832 
833     bool dropDownIsHidden = false;
834     if (IsInDropDownMode()) {
835       dropDownIsHidden = !mComboboxFrame->IsDroppedDown();
836     }
837     if (capturingContent == mContent || dropDownIsHidden) {
838       // only clear the capturing content if *we* are the ones doing the
839       // capturing (or if the dropdown is hidden, in which case NO-ONE should
840       // be capturing anything - it could be a scrollbar inside this listbox
841       // which is actually grabbing
842       // This shouldn't be necessary. We should simply ensure that events
843       // targeting scrollbars are never visible to DOM consumers.
844       PresShell::ReleaseCapturingContent();
845     }
846   }
847 }
848 
849 //---------------------------------------------------------
HandleEvent(nsPresContext * aPresContext,WidgetGUIEvent * aEvent,nsEventStatus * aEventStatus)850 nsresult nsListControlFrame::HandleEvent(nsPresContext* aPresContext,
851                                          WidgetGUIEvent* aEvent,
852                                          nsEventStatus* aEventStatus) {
853   NS_ENSURE_ARG_POINTER(aEventStatus);
854 
855   /*const char * desc[] = {"eMouseMove",
856                           "NS_MOUSE_LEFT_BUTTON_UP",
857                           "NS_MOUSE_LEFT_BUTTON_DOWN",
858                           "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
859                           "NS_MOUSE_MIDDLE_BUTTON_UP",
860                           "NS_MOUSE_MIDDLE_BUTTON_DOWN",
861                           "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
862                           "NS_MOUSE_RIGHT_BUTTON_UP",
863                           "NS_MOUSE_RIGHT_BUTTON_DOWN",
864                           "eMouseOver",
865                           "eMouseOut",
866                           "NS_MOUSE_LEFT_DOUBLECLICK",
867                           "NS_MOUSE_MIDDLE_DOUBLECLICK",
868                           "NS_MOUSE_RIGHT_DOUBLECLICK",
869                           "NS_MOUSE_LEFT_CLICK",
870                           "NS_MOUSE_MIDDLE_CLICK",
871                           "NS_MOUSE_RIGHT_CLICK"};
872   int inx = aEvent->mMessage - eMouseEventFirst;
873   if (inx >= 0 && inx <= (NS_MOUSE_RIGHT_CLICK - eMouseEventFirst)) {
874     printf("Mouse in ListFrame %s [%d]\n", desc[inx], aEvent->mMessage);
875   } else {
876     printf("Mouse in ListFrame <UNKNOWN> [%d]\n", aEvent->mMessage);
877   }*/
878 
879   if (nsEventStatus_eConsumeNoDefault == *aEventStatus) return NS_OK;
880 
881   // disabled state affects how we're selected, but we don't want to go through
882   // nsHTMLScrollFrame if we're disabled.
883   if (IsContentDisabled()) {
884     return nsFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
885   }
886 
887   return nsHTMLScrollFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
888 }
889 
890 //---------------------------------------------------------
SetInitialChildList(ChildListID aListID,nsFrameList & aChildList)891 void nsListControlFrame::SetInitialChildList(ChildListID aListID,
892                                              nsFrameList& aChildList) {
893   if (aListID == kPrincipalList) {
894     // First check to see if all the content has been added
895     mIsAllContentHere = mContent->IsDoneAddingChildren();
896     if (!mIsAllContentHere) {
897       mIsAllFramesHere = false;
898       mHasBeenInitialized = false;
899     }
900   }
901   nsHTMLScrollFrame::SetInitialChildList(aListID, aChildList);
902 
903   // If all the content is here now check
904   // to see if all the frames have been created
905   /*if (mIsAllContentHere) {
906     // If all content and frames are here
907     // the reset/initialize
908     if (CheckIfAllFramesHere()) {
909       ResetList(aPresContext);
910       mHasBeenInitialized = true;
911     }
912   }*/
913 }
914 
915 //---------------------------------------------------------
Init(nsIContent * aContent,nsContainerFrame * aParent,nsIFrame * aPrevInFlow)916 void nsListControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
917                               nsIFrame* aPrevInFlow) {
918   nsHTMLScrollFrame::Init(aContent, aParent, aPrevInFlow);
919 
920   if (IsInDropDownMode()) {
921     AddStateBits(NS_FRAME_IN_POPUP);
922     CreateView();
923   }
924 
925   // we shouldn't have to unregister this listener because when
926   // our frame goes away all these content node go away as well
927   // because our frame is the only one who references them.
928   // we need to hook up our listeners before the editor is initialized
929   mEventListener = new nsListEventListener(this);
930 
931   mContent->AddSystemEventListener(NS_LITERAL_STRING("keydown"), mEventListener,
932                                    false, false);
933   mContent->AddSystemEventListener(NS_LITERAL_STRING("keypress"),
934                                    mEventListener, false, false);
935   mContent->AddSystemEventListener(NS_LITERAL_STRING("mousedown"),
936                                    mEventListener, false, false);
937   mContent->AddSystemEventListener(NS_LITERAL_STRING("mouseup"), mEventListener,
938                                    false, false);
939   mContent->AddSystemEventListener(NS_LITERAL_STRING("mousemove"),
940                                    mEventListener, false, false);
941 
942   mStartSelectionIndex = kNothingSelected;
943   mEndSelectionIndex = kNothingSelected;
944 
945   mLastDropdownBackstopColor = PresContext()->DefaultBackgroundColor();
946 }
947 
GetOptions() const948 dom::HTMLOptionsCollection* nsListControlFrame::GetOptions() const {
949   dom::HTMLSelectElement* select =
950       dom::HTMLSelectElement::FromNodeOrNull(mContent);
951   NS_ENSURE_TRUE(select, nullptr);
952 
953   return select->Options();
954 }
955 
GetOption(uint32_t aIndex) const956 dom::HTMLOptionElement* nsListControlFrame::GetOption(uint32_t aIndex) const {
957   dom::HTMLSelectElement* select =
958       dom::HTMLSelectElement::FromNodeOrNull(mContent);
959   NS_ENSURE_TRUE(select, nullptr);
960 
961   return select->Item(aIndex);
962 }
963 
964 NS_IMETHODIMP
OnOptionSelected(int32_t aIndex,bool aSelected)965 nsListControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) {
966   if (aSelected) {
967     ScrollToIndex(aIndex);
968   }
969   return NS_OK;
970 }
971 
OnContentReset()972 void nsListControlFrame::OnContentReset() { ResetList(true); }
973 
ResetList(bool aAllowScrolling)974 void nsListControlFrame::ResetList(bool aAllowScrolling) {
975   // if all the frames aren't here
976   // don't bother reseting
977   if (!mIsAllFramesHere) {
978     return;
979   }
980 
981   if (aAllowScrolling) {
982     mPostChildrenLoadedReset = true;
983 
984     // Scroll to the selected index
985     int32_t indexToSelect = kNothingSelected;
986 
987     HTMLSelectElement* selectElement = HTMLSelectElement::FromNode(mContent);
988     if (selectElement) {
989       indexToSelect = selectElement->SelectedIndex();
990       AutoWeakFrame weakFrame(this);
991       ScrollToIndex(indexToSelect);
992       if (!weakFrame.IsAlive()) {
993         return;
994       }
995     }
996   }
997 
998   mStartSelectionIndex = kNothingSelected;
999   mEndSelectionIndex = kNothingSelected;
1000   InvalidateFocus();
1001   // Combobox will redisplay itself with the OnOptionSelected event
1002 }
1003 
SetFocus(bool aOn,bool aRepaint)1004 void nsListControlFrame::SetFocus(bool aOn, bool aRepaint) {
1005   InvalidateFocus();
1006 
1007   if (aOn) {
1008     ComboboxFocusSet();
1009     mFocused = this;
1010   } else {
1011     mFocused = nullptr;
1012   }
1013 
1014   InvalidateFocus();
1015 }
1016 
ComboboxFocusSet()1017 void nsListControlFrame::ComboboxFocusSet() { gLastKeyTime = 0; }
1018 
SetComboboxFrame(nsIFrame * aComboboxFrame)1019 void nsListControlFrame::SetComboboxFrame(nsIFrame* aComboboxFrame) {
1020   if (nullptr != aComboboxFrame) {
1021     mComboboxFrame = do_QueryFrame(aComboboxFrame);
1022   }
1023 }
1024 
GetOptionText(uint32_t aIndex,nsAString & aStr)1025 void nsListControlFrame::GetOptionText(uint32_t aIndex, nsAString& aStr) {
1026   aStr.Truncate();
1027   if (dom::HTMLOptionElement* optionElement = GetOption(aIndex)) {
1028     optionElement->GetRenderedLabel(aStr);
1029   }
1030 }
1031 
GetSelectedIndex()1032 int32_t nsListControlFrame::GetSelectedIndex() {
1033   dom::HTMLSelectElement* select =
1034       dom::HTMLSelectElement::FromNodeOrNull(mContent);
1035   return select->SelectedIndex();
1036 }
1037 
GetCurrentOption()1038 dom::HTMLOptionElement* nsListControlFrame::GetCurrentOption() {
1039   // The mEndSelectionIndex is what is currently being selected. Use
1040   // the selected index if this is kNothingSelected.
1041   int32_t focusedIndex = (mEndSelectionIndex == kNothingSelected)
1042                              ? GetSelectedIndex()
1043                              : mEndSelectionIndex;
1044 
1045   if (focusedIndex != kNothingSelected) {
1046     return GetOption(AssertedCast<uint32_t>(focusedIndex));
1047   }
1048 
1049   // There is no selected option. Return the first non-disabled option, if any.
1050   return GetNonDisabledOptionFrom(0);
1051 }
1052 
GetNonDisabledOptionFrom(int32_t aFromIndex,int32_t * aFoundIndex)1053 HTMLOptionElement* nsListControlFrame::GetNonDisabledOptionFrom(
1054     int32_t aFromIndex, int32_t* aFoundIndex) {
1055   RefPtr<dom::HTMLSelectElement> selectElement =
1056       dom::HTMLSelectElement::FromNode(mContent);
1057 
1058   const uint32_t length = selectElement->Length();
1059   for (uint32_t i = std::max(aFromIndex, 0); i < length; ++i) {
1060     HTMLOptionElement* node = selectElement->Item(i);
1061     if (!node) {
1062       break;
1063     }
1064     if (IsOptionInteractivelySelectable(selectElement, node)) {
1065       if (aFoundIndex) {
1066         *aFoundIndex = i;
1067       }
1068       return node;
1069     }
1070   }
1071   return nullptr;
1072 }
1073 
IsInDropDownMode() const1074 bool nsListControlFrame::IsInDropDownMode() const {
1075   return (mComboboxFrame != nullptr);
1076 }
1077 
GetNumberOfOptions()1078 uint32_t nsListControlFrame::GetNumberOfOptions() {
1079   dom::HTMLOptionsCollection* options = GetOptions();
1080   if (!options) {
1081     return 0;
1082   }
1083 
1084   return options->Length();
1085 }
1086 
1087 //----------------------------------------------------------------------
1088 // nsISelectControlFrame
1089 //----------------------------------------------------------------------
CheckIfAllFramesHere()1090 bool nsListControlFrame::CheckIfAllFramesHere() {
1091   // XXX Need to find a fail proof way to determine that
1092   // all the frames are there
1093   mIsAllFramesHere = true;
1094 
1095   // now make sure we have a frame each piece of content
1096 
1097   return mIsAllFramesHere;
1098 }
1099 
1100 NS_IMETHODIMP
DoneAddingChildren(bool aIsDone)1101 nsListControlFrame::DoneAddingChildren(bool aIsDone) {
1102   mIsAllContentHere = aIsDone;
1103   if (mIsAllContentHere) {
1104     // Here we check to see if all the frames have been created
1105     // for all the content.
1106     // If so, then we can initialize;
1107     if (!mIsAllFramesHere) {
1108       // if all the frames are now present we can initialize
1109       if (CheckIfAllFramesHere()) {
1110         mHasBeenInitialized = true;
1111         ResetList(true);
1112       }
1113     }
1114   }
1115   return NS_OK;
1116 }
1117 
1118 NS_IMETHODIMP
AddOption(int32_t aIndex)1119 nsListControlFrame::AddOption(int32_t aIndex) {
1120 #ifdef DO_REFLOW_DEBUG
1121   printf("---- Id: %d nsLCF %p Added Option %d\n", mReflowId, this, aIndex);
1122 #endif
1123 
1124   if (!mIsAllContentHere) {
1125     mIsAllContentHere = mContent->IsDoneAddingChildren();
1126     if (!mIsAllContentHere) {
1127       mIsAllFramesHere = false;
1128       mHasBeenInitialized = false;
1129     } else {
1130       mIsAllFramesHere =
1131           (aIndex == static_cast<int32_t>(GetNumberOfOptions() - 1));
1132     }
1133   }
1134 
1135   // Make sure we scroll to the selected option as needed
1136   mNeedToReset = true;
1137 
1138   if (!mHasBeenInitialized) {
1139     return NS_OK;
1140   }
1141 
1142   mPostChildrenLoadedReset = mIsAllContentHere;
1143   return NS_OK;
1144 }
1145 
DecrementAndClamp(int32_t aSelectionIndex,int32_t aLength)1146 static int32_t DecrementAndClamp(int32_t aSelectionIndex, int32_t aLength) {
1147   return aLength == 0 ? kNothingSelected : std::max(0, aSelectionIndex - 1);
1148 }
1149 
1150 NS_IMETHODIMP
RemoveOption(int32_t aIndex)1151 nsListControlFrame::RemoveOption(int32_t aIndex) {
1152   MOZ_ASSERT(aIndex >= 0, "negative <option> index");
1153 
1154   // Need to reset if we're a dropdown
1155   if (IsInDropDownMode()) {
1156     mNeedToReset = true;
1157     mPostChildrenLoadedReset = mIsAllContentHere;
1158   }
1159 
1160   if (mStartSelectionIndex != kNothingSelected) {
1161     NS_ASSERTION(mEndSelectionIndex != kNothingSelected, "");
1162     int32_t numOptions = GetNumberOfOptions();
1163     // NOTE: numOptions is the new number of options whereas aIndex is the
1164     // unadjusted index of the removed option (hence the <= below).
1165     NS_ASSERTION(aIndex <= numOptions, "out-of-bounds <option> index");
1166 
1167     int32_t forward = mEndSelectionIndex - mStartSelectionIndex;
1168     int32_t* low = forward >= 0 ? &mStartSelectionIndex : &mEndSelectionIndex;
1169     int32_t* high = forward >= 0 ? &mEndSelectionIndex : &mStartSelectionIndex;
1170     if (aIndex < *low) *low = ::DecrementAndClamp(*low, numOptions);
1171     if (aIndex <= *high) *high = ::DecrementAndClamp(*high, numOptions);
1172     if (forward == 0) *low = *high;
1173   } else
1174     NS_ASSERTION(mEndSelectionIndex == kNothingSelected, "");
1175 
1176   InvalidateFocus();
1177   return NS_OK;
1178 }
1179 
1180 //---------------------------------------------------------
1181 // Set the option selected in the DOM.  This method is named
1182 // as it is because it indicates that the frame is the source
1183 // of this event rather than the receiver.
SetOptionsSelectedFromFrame(int32_t aStartIndex,int32_t aEndIndex,bool aValue,bool aClearAll)1184 bool nsListControlFrame::SetOptionsSelectedFromFrame(int32_t aStartIndex,
1185                                                      int32_t aEndIndex,
1186                                                      bool aValue,
1187                                                      bool aClearAll) {
1188   RefPtr<dom::HTMLSelectElement> selectElement =
1189       dom::HTMLSelectElement::FromNode(mContent);
1190 
1191   uint32_t mask = dom::HTMLSelectElement::NOTIFY;
1192   if (mForceSelection) {
1193     mask |= dom::HTMLSelectElement::SET_DISABLED;
1194   }
1195   if (aValue) {
1196     mask |= dom::HTMLSelectElement::IS_SELECTED;
1197   }
1198   if (aClearAll) {
1199     mask |= dom::HTMLSelectElement::CLEAR_ALL;
1200   }
1201 
1202   return selectElement->SetOptionsSelectedByIndex(aStartIndex, aEndIndex, mask);
1203 }
1204 
ToggleOptionSelectedFromFrame(int32_t aIndex)1205 bool nsListControlFrame::ToggleOptionSelectedFromFrame(int32_t aIndex) {
1206   RefPtr<dom::HTMLOptionElement> option =
1207       GetOption(static_cast<uint32_t>(aIndex));
1208   NS_ENSURE_TRUE(option, false);
1209 
1210   RefPtr<dom::HTMLSelectElement> selectElement =
1211       dom::HTMLSelectElement::FromNode(mContent);
1212 
1213   uint32_t mask = dom::HTMLSelectElement::NOTIFY;
1214   if (!option->Selected()) {
1215     mask |= dom::HTMLSelectElement::IS_SELECTED;
1216   }
1217 
1218   return selectElement->SetOptionsSelectedByIndex(aIndex, aIndex, mask);
1219 }
1220 
1221 // Dispatch event and such
UpdateSelection()1222 bool nsListControlFrame::UpdateSelection() {
1223   if (mIsAllFramesHere) {
1224     // if it's a combobox, display the new text
1225     AutoWeakFrame weakFrame(this);
1226     if (mComboboxFrame) {
1227       mComboboxFrame->RedisplaySelectedText();
1228 
1229       // When dropdown list is open, onchange event will be fired when Enter key
1230       // is hit or when dropdown list is dismissed.
1231       if (mComboboxFrame->IsDroppedDown()) {
1232         return weakFrame.IsAlive();
1233       }
1234     }
1235     if (mIsAllContentHere) {
1236       FireOnInputAndOnChange();
1237     }
1238     return weakFrame.IsAlive();
1239   }
1240   return true;
1241 }
1242 
ComboboxFinish(int32_t aIndex)1243 void nsListControlFrame::ComboboxFinish(int32_t aIndex) {
1244   gLastKeyTime = 0;
1245 
1246   if (mComboboxFrame) {
1247     int32_t displayIndex = mComboboxFrame->GetIndexOfDisplayArea();
1248     // Make sure we can always reset to the displayed index
1249     mForceSelection = displayIndex == aIndex;
1250 
1251     AutoWeakFrame weakFrame(this);
1252     PerformSelection(aIndex, false, false);  // might destroy us
1253     if (!weakFrame.IsAlive() || !mComboboxFrame) {
1254       return;
1255     }
1256 
1257     if (displayIndex != aIndex) {
1258       mComboboxFrame->RedisplaySelectedText();  // might destroy us
1259     }
1260 
1261     if (weakFrame.IsAlive() && mComboboxFrame) {
1262       mComboboxFrame->RollupFromList();  // might destroy us
1263     }
1264   }
1265 }
1266 
1267 // Send out an onInput and onChange notification.
FireOnInputAndOnChange()1268 void nsListControlFrame::FireOnInputAndOnChange() {
1269   if (mComboboxFrame) {
1270     // Return hit without changing anything
1271     int32_t index = mComboboxFrame->UpdateRecentIndex(NS_SKIP_NOTIFY_INDEX);
1272     if (index == NS_SKIP_NOTIFY_INDEX) {
1273       return;
1274     }
1275 
1276     // See if the selection actually changed
1277     if (index == GetSelectedIndex()) {
1278       return;
1279     }
1280   }
1281 
1282   RefPtr<Element> element = Element::FromNodeOrNull(mContent);
1283   if (NS_WARN_IF(!element)) {
1284     return;
1285   }
1286   // Dispatch the input event.
1287   DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(element);
1288   NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
1289                        "Failed to dispatch input event");
1290 
1291   // Dispatch the change event.
1292   nsContentUtils::DispatchTrustedEvent(element->OwnerDoc(), element,
1293                                        NS_LITERAL_STRING("change"),
1294                                        CanBubble::eYes, Cancelable::eNo);
1295 }
1296 
NS_IMETHODIMP_(void)1297 NS_IMETHODIMP_(void)
1298 nsListControlFrame::OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) {
1299   if (mComboboxFrame) {
1300     // UpdateRecentIndex with NS_SKIP_NOTIFY_INDEX, so that we won't fire an
1301     // onchange event for this setting of selectedIndex.
1302     mComboboxFrame->UpdateRecentIndex(NS_SKIP_NOTIFY_INDEX);
1303   }
1304 
1305   AutoWeakFrame weakFrame(this);
1306   ScrollToIndex(aNewIndex);
1307   if (!weakFrame.IsAlive()) {
1308     return;
1309   }
1310   mStartSelectionIndex = aNewIndex;
1311   mEndSelectionIndex = aNewIndex;
1312   InvalidateFocus();
1313 
1314 #ifdef ACCESSIBILITY
1315   FireMenuItemActiveEvent();
1316 #endif
1317 }
1318 
1319 //----------------------------------------------------------------------
1320 // End nsISelectControlFrame
1321 //----------------------------------------------------------------------
1322 
SetFormProperty(nsAtom * aName,const nsAString & aValue)1323 nsresult nsListControlFrame::SetFormProperty(nsAtom* aName,
1324                                              const nsAString& aValue) {
1325   if (nsGkAtoms::selected == aName) {
1326     return NS_ERROR_INVALID_ARG;  // Selected is readonly according to spec.
1327   } else if (nsGkAtoms::selectedindex == aName) {
1328     // You shouldn't be calling me for this!!!
1329     return NS_ERROR_INVALID_ARG;
1330   }
1331 
1332   // We should be told about selectedIndex by the DOM element through
1333   // OnOptionSelected
1334 
1335   return NS_OK;
1336 }
1337 
AboutToDropDown()1338 void nsListControlFrame::AboutToDropDown() {
1339   NS_ASSERTION(IsInDropDownMode(),
1340                "AboutToDropDown called without being in dropdown mode");
1341 
1342   // Our widget doesn't get invalidated on changes to the rest of the document,
1343   // so compute and store this color at the start of a dropdown so we don't
1344   // get weird painting behaviour.
1345   // We start looking for backgrounds above the combobox frame to avoid
1346   // duplicating the combobox frame's background and compose each background
1347   // color we find underneath until we have an opaque color, or run out of
1348   // backgrounds. We compose with the PresContext default background color,
1349   // which is always opaque, in case we don't end up with an opaque color.
1350   // This gives us a very poor approximation of translucency.
1351   nsIFrame* comboboxFrame = do_QueryFrame(mComboboxFrame);
1352   nsIFrame* ancestor = comboboxFrame->GetParent();
1353   mLastDropdownBackstopColor = NS_RGBA(0, 0, 0, 0);
1354   while (NS_GET_A(mLastDropdownBackstopColor) < 255 && ancestor) {
1355     ComputedStyle* context = ancestor->Style();
1356     mLastDropdownBackstopColor =
1357         NS_ComposeColors(context->StyleBackground()->BackgroundColor(context),
1358                          mLastDropdownBackstopColor);
1359     ancestor = ancestor->GetParent();
1360   }
1361   mLastDropdownBackstopColor = NS_ComposeColors(
1362       PresContext()->DefaultBackgroundColor(), mLastDropdownBackstopColor);
1363 
1364   if (mIsAllContentHere && mIsAllFramesHere && mHasBeenInitialized) {
1365     AutoWeakFrame weakFrame(this);
1366     ScrollToIndex(GetSelectedIndex());
1367     if (!weakFrame.IsAlive()) {
1368       return;
1369     }
1370 #ifdef ACCESSIBILITY
1371     FireMenuItemActiveEvent();  // Inform assistive tech what got focus
1372 #endif
1373   }
1374   mItemSelectionStarted = false;
1375   mForceSelection = false;
1376 }
1377 
1378 // We are about to be rolledup from the outside (ComboboxFrame)
AboutToRollup()1379 void nsListControlFrame::AboutToRollup() {
1380   // We've been updating the combobox with the keyboard up until now, but not
1381   // with the mouse.  The problem is, even with mouse selection, we are
1382   // updating the <select>.  So if the mouse goes over an option just before
1383   // he leaves the box and clicks, that's what the <select> will show.
1384   //
1385   // To deal with this we say "whatever is in the combobox is canonical."
1386   // - IF the combobox is different from the current selected index, we
1387   //   reset the index.
1388 
1389   if (IsInDropDownMode()) {
1390     ComboboxFinish(
1391         mComboboxFrame->GetIndexOfDisplayArea());  // might destroy us
1392   }
1393 }
1394 
DidReflow(nsPresContext * aPresContext,const ReflowInput * aReflowInput)1395 void nsListControlFrame::DidReflow(nsPresContext* aPresContext,
1396                                    const ReflowInput* aReflowInput) {
1397   bool wasInterrupted = !mHasPendingInterruptAtStartOfReflow &&
1398                         aPresContext->HasPendingInterrupt();
1399 
1400   nsHTMLScrollFrame::DidReflow(aPresContext, aReflowInput);
1401 
1402   if (mNeedToReset && !wasInterrupted) {
1403     mNeedToReset = false;
1404     // Suppress scrolling to the selected element if we restored
1405     // scroll history state AND the list contents have not changed
1406     // since we loaded all the children AND nothing else forced us
1407     // to scroll by calling ResetList(true). The latter two conditions
1408     // are folded into mPostChildrenLoadedReset.
1409     //
1410     // The idea is that we want scroll history restoration to trump ResetList
1411     // scrolling to the selected element, when the ResetList was probably only
1412     // caused by content loading normally.
1413     ResetList(!DidHistoryRestore() || mPostChildrenLoadedReset);
1414   }
1415 
1416   mHasPendingInterruptAtStartOfReflow = false;
1417 }
1418 
1419 #ifdef DEBUG_FRAME_DUMP
GetFrameName(nsAString & aResult) const1420 nsresult nsListControlFrame::GetFrameName(nsAString& aResult) const {
1421   return MakeFrameName(NS_LITERAL_STRING("ListControl"), aResult);
1422 }
1423 #endif
1424 
GetBSizeOfARow()1425 nscoord nsListControlFrame::GetBSizeOfARow() { return BSizeOfARow(); }
1426 
IsOptionInteractivelySelectable(int32_t aIndex) const1427 bool nsListControlFrame::IsOptionInteractivelySelectable(int32_t aIndex) const {
1428   if (HTMLSelectElement* sel = HTMLSelectElement::FromNode(mContent)) {
1429     if (HTMLOptionElement* item = sel->Item(aIndex)) {
1430       return IsOptionInteractivelySelectable(sel, item);
1431     }
1432   }
1433   return false;
1434 }
1435 
IsOptionInteractivelySelectable(HTMLSelectElement * aSelect,HTMLOptionElement * aOption)1436 bool nsListControlFrame::IsOptionInteractivelySelectable(
1437     HTMLSelectElement* aSelect, HTMLOptionElement* aOption) {
1438   return !aSelect->IsOptionDisabled(aOption) && aOption->GetPrimaryFrame();
1439 }
1440 
1441 //----------------------------------------------------------------------
1442 // helper
1443 //----------------------------------------------------------------------
IsLeftButton(dom::Event * aMouseEvent)1444 bool nsListControlFrame::IsLeftButton(dom::Event* aMouseEvent) {
1445   // only allow selection with the left button
1446   MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
1447   return mouseEvent && mouseEvent->Button() == 0;
1448 }
1449 
CalcFallbackRowBSize(float aFontSizeInflation)1450 nscoord nsListControlFrame::CalcFallbackRowBSize(float aFontSizeInflation) {
1451   RefPtr<nsFontMetrics> fontMet =
1452       nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation);
1453   return fontMet->MaxHeight();
1454 }
1455 
CalcIntrinsicBSize(nscoord aBSizeOfARow,int32_t aNumberOfOptions)1456 nscoord nsListControlFrame::CalcIntrinsicBSize(nscoord aBSizeOfARow,
1457                                                int32_t aNumberOfOptions) {
1458   MOZ_ASSERT(!IsInDropDownMode(),
1459              "Shouldn't be in dropdown mode when we call this");
1460 
1461   dom::HTMLSelectElement* select =
1462       dom::HTMLSelectElement::FromNodeOrNull(mContent);
1463   if (select) {
1464     mNumDisplayRows = select->Size();
1465   } else {
1466     mNumDisplayRows = 1;
1467   }
1468 
1469   if (mNumDisplayRows < 1) {
1470     mNumDisplayRows = 4;
1471   }
1472 
1473   return mNumDisplayRows * aBSizeOfARow;
1474 }
1475 
1476 //----------------------------------------------------------------------
1477 // nsIDOMMouseListener
1478 //----------------------------------------------------------------------
MouseUp(dom::Event * aMouseEvent)1479 nsresult nsListControlFrame::MouseUp(dom::Event* aMouseEvent) {
1480   NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
1481 
1482   MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
1483   NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
1484 
1485   UpdateInListState(aMouseEvent);
1486 
1487   mButtonDown = false;
1488 
1489   EventStates eventStates = mContent->AsElement()->State();
1490   if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
1491     return NS_OK;
1492   }
1493 
1494   // only allow selection with the left button
1495   // if a right button click is on the combobox itself
1496   // or on the select when in listbox mode, then let the click through
1497   if (!IsLeftButton(aMouseEvent)) {
1498     if (IsInDropDownMode()) {
1499       if (!IgnoreMouseEventForSelection(aMouseEvent)) {
1500         aMouseEvent->PreventDefault();
1501         aMouseEvent->StopPropagation();
1502       } else {
1503         CaptureMouseEvents(false);
1504         return NS_OK;
1505       }
1506       CaptureMouseEvents(false);
1507       return NS_ERROR_FAILURE;  // means consume event
1508     } else {
1509       CaptureMouseEvents(false);
1510       return NS_OK;
1511     }
1512   }
1513 
1514   const nsStyleVisibility* vis = StyleVisibility();
1515 
1516   if (!vis->IsVisible()) {
1517     return NS_OK;
1518   }
1519 
1520   if (IsInDropDownMode()) {
1521     // XXX This is a bit of a hack, but.....
1522     // But the idea here is to make sure you get an "onclick" event when you
1523     // mouse down on the select and the drag over an option and let go And then
1524     // NOT get an "onclick" event when when you click down on the select and
1525     // then up outside of the select the EventStateManager tracks the content of
1526     // the mouse down and the mouse up to make sure they are the same, and the
1527     // onclick is sent in the PostHandleEvent depeneding on whether the
1528     // clickCount is non-zero. So we cheat here by either setting or unsetting
1529     // the clcikCount in the native event so the right thing happens for the
1530     // onclick event
1531     WidgetMouseEvent* mouseEvent =
1532         aMouseEvent->WidgetEventPtr()->AsMouseEvent();
1533 
1534     int32_t selectedIndex;
1535     if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
1536       // If it's not selectable, disallow the click and leave.
1537       if (!IsOptionInteractivelySelectable(selectedIndex)) {
1538         aMouseEvent->PreventDefault();
1539         aMouseEvent->StopPropagation();
1540         CaptureMouseEvents(false);
1541         return NS_ERROR_FAILURE;
1542       }
1543 
1544       if (kNothingSelected != selectedIndex) {
1545         AutoWeakFrame weakFrame(this);
1546         ComboboxFinish(selectedIndex);
1547         if (!weakFrame.IsAlive()) {
1548           return NS_OK;
1549         }
1550 
1551         FireOnInputAndOnChange();
1552       }
1553 
1554       mouseEvent->mClickCount = 1;
1555     } else {
1556       // the click was out side of the select or its dropdown
1557       mouseEvent->mClickCount =
1558           IgnoreMouseEventForSelection(aMouseEvent) ? 1 : 0;
1559     }
1560   } else {
1561     CaptureMouseEvents(false);
1562     // Notify
1563     if (mChangesSinceDragStart) {
1564       // reset this so that future MouseUps without a prior MouseDown
1565       // won't fire onchange
1566       mChangesSinceDragStart = false;
1567       FireOnInputAndOnChange();
1568     }
1569   }
1570 
1571   return NS_OK;
1572 }
1573 
UpdateInListState(dom::Event * aEvent)1574 void nsListControlFrame::UpdateInListState(dom::Event* aEvent) {
1575   if (!mComboboxFrame || !mComboboxFrame->IsDroppedDown()) return;
1576 
1577   nsPoint pt = nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aEvent, this);
1578   nsRect borderInnerEdge = GetScrollPortRect();
1579   if (pt.y >= borderInnerEdge.y && pt.y < borderInnerEdge.YMost()) {
1580     mItemSelectionStarted = true;
1581   }
1582 }
1583 
IgnoreMouseEventForSelection(dom::Event * aEvent)1584 bool nsListControlFrame::IgnoreMouseEventForSelection(dom::Event* aEvent) {
1585   if (!mComboboxFrame) return false;
1586 
1587   // Our DOM listener does get called when the dropdown is not
1588   // showing, because it listens to events on the SELECT element
1589   if (!mComboboxFrame->IsDroppedDown()) return true;
1590 
1591   return !mItemSelectionStarted;
1592 }
1593 
1594 #ifdef ACCESSIBILITY
FireMenuItemActiveEvent()1595 void nsListControlFrame::FireMenuItemActiveEvent() {
1596   if (mFocused != this && !IsInDropDownMode()) {
1597     return;
1598   }
1599 
1600   nsCOMPtr<nsIContent> optionContent = GetCurrentOption();
1601   if (!optionContent) {
1602     return;
1603   }
1604 
1605   FireDOMEvent(NS_LITERAL_STRING("DOMMenuItemActive"), optionContent);
1606 }
1607 #endif
1608 
GetIndexFromDOMEvent(dom::Event * aMouseEvent,int32_t & aCurIndex)1609 nsresult nsListControlFrame::GetIndexFromDOMEvent(dom::Event* aMouseEvent,
1610                                                   int32_t& aCurIndex) {
1611   if (IgnoreMouseEventForSelection(aMouseEvent)) return NS_ERROR_FAILURE;
1612 
1613   if (PresShell::GetCapturingContent() != mContent) {
1614     // If we're not capturing, then ignore movement in the border
1615     nsPoint pt =
1616         nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aMouseEvent, this);
1617     nsRect borderInnerEdge = GetScrollPortRect();
1618     if (!borderInnerEdge.Contains(pt)) {
1619       return NS_ERROR_FAILURE;
1620     }
1621   }
1622 
1623   RefPtr<dom::HTMLOptionElement> option;
1624   for (nsCOMPtr<nsIContent> content =
1625            PresContext()->EventStateManager()->GetEventTargetContent(nullptr);
1626        content && !option; content = content->GetParent()) {
1627     option = dom::HTMLOptionElement::FromNode(content);
1628   }
1629 
1630   if (option) {
1631     aCurIndex = option->Index();
1632     MOZ_ASSERT(aCurIndex >= 0);
1633     return NS_OK;
1634   }
1635 
1636   return NS_ERROR_FAILURE;
1637 }
1638 
FireShowDropDownEvent(nsIContent * aContent,bool aShow,bool aIsSourceTouchEvent)1639 static bool FireShowDropDownEvent(nsIContent* aContent, bool aShow,
1640                                   bool aIsSourceTouchEvent) {
1641   if (ShouldFireDropDownEvent()) {
1642     nsString eventName;
1643     if (aShow) {
1644       eventName = aIsSourceTouchEvent
1645                       ? NS_LITERAL_STRING("mozshowdropdown-sourcetouch")
1646                       : NS_LITERAL_STRING("mozshowdropdown");
1647     } else {
1648       eventName = NS_LITERAL_STRING("mozhidedropdown");
1649     }
1650     nsContentUtils::DispatchChromeEvent(aContent->OwnerDoc(), aContent,
1651                                         eventName, CanBubble::eYes,
1652                                         Cancelable::eNo);
1653     return true;
1654   }
1655 
1656   return false;
1657 }
1658 
MouseDown(dom::Event * aMouseEvent)1659 nsresult nsListControlFrame::MouseDown(dom::Event* aMouseEvent) {
1660   NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
1661 
1662   MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
1663   NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
1664 
1665   UpdateInListState(aMouseEvent);
1666 
1667   EventStates eventStates = mContent->AsElement()->State();
1668   if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
1669     return NS_OK;
1670   }
1671 
1672   // only allow selection with the left button
1673   // if a right button click is on the combobox itself
1674   // or on the select when in listbox mode, then let the click through
1675   if (!IsLeftButton(aMouseEvent)) {
1676     if (IsInDropDownMode()) {
1677       if (!IgnoreMouseEventForSelection(aMouseEvent)) {
1678         aMouseEvent->PreventDefault();
1679         aMouseEvent->StopPropagation();
1680       } else {
1681         return NS_OK;
1682       }
1683       return NS_ERROR_FAILURE;  // means consume event
1684     } else {
1685       return NS_OK;
1686     }
1687   }
1688 
1689   int32_t selectedIndex;
1690   if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
1691     // Handle Like List
1692     mButtonDown = true;
1693     CaptureMouseEvents(true);
1694     AutoWeakFrame weakFrame(this);
1695     bool change =
1696         HandleListSelection(aMouseEvent, selectedIndex);  // might destroy us
1697     if (!weakFrame.IsAlive()) {
1698       return NS_OK;
1699     }
1700     mChangesSinceDragStart = change;
1701   } else {
1702     // NOTE: the combo box is responsible for dropping it down
1703     if (mComboboxFrame) {
1704       // Ignore the click that occurs on the option element when one is
1705       // selected from the parent process popup.
1706       if (mComboboxFrame->IsOpenInParentProcess()) {
1707         nsCOMPtr<nsIContent> econtent =
1708             do_QueryInterface(aMouseEvent->GetTarget());
1709         HTMLOptionElement* option = HTMLOptionElement::FromNodeOrNull(econtent);
1710         if (option) {
1711           return NS_OK;
1712         }
1713       }
1714 
1715       uint16_t inputSource = mouseEvent->MozInputSource();
1716       bool isSourceTouchEvent =
1717           inputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH;
1718       if (FireShowDropDownEvent(
1719               mContent, !mComboboxFrame->IsDroppedDownOrHasParentPopup(),
1720               isSourceTouchEvent)) {
1721         return NS_OK;
1722       }
1723 
1724       if (!IgnoreMouseEventForSelection(aMouseEvent)) {
1725         return NS_OK;
1726       }
1727 
1728       if (!nsComboboxControlFrame::ToolkitHasNativePopup()) {
1729         bool isDroppedDown = mComboboxFrame->IsDroppedDown();
1730         nsIFrame* comboFrame = do_QueryFrame(mComboboxFrame);
1731         AutoWeakFrame weakFrame(comboFrame);
1732         mComboboxFrame->ShowDropDown(!isDroppedDown);
1733         if (!weakFrame.IsAlive()) return NS_OK;
1734         if (isDroppedDown) {
1735           CaptureMouseEvents(false);
1736         }
1737       }
1738     }
1739   }
1740 
1741   return NS_OK;
1742 }
1743 
MouseMove(dom::Event * aMouseEvent)1744 nsresult nsListControlFrame::MouseMove(dom::Event* aMouseEvent) {
1745   NS_ASSERTION(aMouseEvent, "aMouseEvent is null.");
1746   MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
1747   NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
1748 
1749   UpdateInListState(aMouseEvent);
1750 
1751   if (IsInDropDownMode()) {
1752     if (mComboboxFrame->IsDroppedDown()) {
1753       int32_t selectedIndex;
1754       if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
1755         PerformSelection(selectedIndex, false, false);  // might destroy us
1756       }
1757     }
1758   } else {  // XXX - temporary until we get drag events
1759     if (mButtonDown) {
1760       return DragMove(aMouseEvent);  // might destroy us
1761     }
1762   }
1763   return NS_OK;
1764 }
1765 
DragMove(dom::Event * aMouseEvent)1766 nsresult nsListControlFrame::DragMove(dom::Event* aMouseEvent) {
1767   NS_ASSERTION(aMouseEvent, "aMouseEvent is null.");
1768 
1769   UpdateInListState(aMouseEvent);
1770 
1771   if (!IsInDropDownMode()) {
1772     int32_t selectedIndex;
1773     if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
1774       // Don't waste cycles if we already dragged over this item
1775       if (selectedIndex == mEndSelectionIndex) {
1776         return NS_OK;
1777       }
1778       MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
1779       NS_ASSERTION(mouseEvent, "aMouseEvent is not a MouseEvent!");
1780       bool isControl;
1781 #ifdef XP_MACOSX
1782       isControl = mouseEvent->MetaKey();
1783 #else
1784       isControl = mouseEvent->CtrlKey();
1785 #endif
1786       AutoWeakFrame weakFrame(this);
1787       // Turn SHIFT on when you are dragging, unless control is on.
1788       bool wasChanged = PerformSelection(selectedIndex, !isControl, isControl);
1789       if (!weakFrame.IsAlive()) {
1790         return NS_OK;
1791       }
1792       mChangesSinceDragStart = mChangesSinceDragStart || wasChanged;
1793     }
1794   }
1795   return NS_OK;
1796 }
1797 
1798 //----------------------------------------------------------------------
1799 // Scroll helpers.
1800 //----------------------------------------------------------------------
ScrollToIndex(int32_t aIndex)1801 void nsListControlFrame::ScrollToIndex(int32_t aIndex) {
1802   if (aIndex < 0) {
1803     // XXX shouldn't we just do nothing if we're asked to scroll to
1804     // kNothingSelected?
1805     ScrollTo(nsPoint(0, 0), ScrollMode::Instant);
1806   } else {
1807     RefPtr<dom::HTMLOptionElement> option =
1808         GetOption(AssertedCast<uint32_t>(aIndex));
1809     if (option) {
1810       ScrollToFrame(*option);
1811     }
1812   }
1813 }
1814 
ScrollToFrame(dom::HTMLOptionElement & aOptElement)1815 void nsListControlFrame::ScrollToFrame(dom::HTMLOptionElement& aOptElement) {
1816   // otherwise we find the content's frame and scroll to it
1817   if (nsIFrame* childFrame = aOptElement.GetPrimaryFrame()) {
1818     RefPtr<mozilla::PresShell> presShell = PresShell();
1819     presShell->ScrollFrameRectIntoView(
1820         childFrame, nsRect(nsPoint(0, 0), childFrame->GetSize()), ScrollAxis(),
1821         ScrollAxis(),
1822         ScrollFlags::ScrollOverflowHidden |
1823             ScrollFlags::ScrollFirstAncestorOnly);
1824   }
1825 }
1826 
1827 //---------------------------------------------------------------------
1828 // Ok, the entire idea of this routine is to move to the next item that
1829 // is suppose to be selected. If the item is disabled then we search in
1830 // the same direction looking for the next item to select. If we run off
1831 // the end of the list then we start at the end of the list and search
1832 // backwards until we get back to the original item or an enabled option
1833 //
1834 // aStartIndex - the index to start searching from
1835 // aNewIndex - will get set to the new index if it finds one
1836 // aNumOptions - the total number of options in the list
1837 // aDoAdjustInc - the initial increment 1-n
1838 // aDoAdjustIncNext - the increment used to search for the next enabled option
1839 //
1840 // the aDoAdjustInc could be a "1" for a single item or
1841 // any number greater representing a page of items
1842 //
AdjustIndexForDisabledOpt(int32_t aStartIndex,int32_t & aNewIndex,int32_t aNumOptions,int32_t aDoAdjustInc,int32_t aDoAdjustIncNext)1843 void nsListControlFrame::AdjustIndexForDisabledOpt(int32_t aStartIndex,
1844                                                    int32_t& aNewIndex,
1845                                                    int32_t aNumOptions,
1846                                                    int32_t aDoAdjustInc,
1847                                                    int32_t aDoAdjustIncNext) {
1848   // Cannot select anything if there is nothing to select
1849   if (aNumOptions == 0) {
1850     aNewIndex = kNothingSelected;
1851     return;
1852   }
1853 
1854   // means we reached the end of the list and now we are searching backwards
1855   bool doingReverse = false;
1856   // lowest index in the search range
1857   int32_t bottom = 0;
1858   // highest index in the search range
1859   int32_t top = aNumOptions;
1860 
1861   // Start off keyboard options at selectedIndex if nothing else is defaulted to
1862   //
1863   // XXX Perhaps this should happen for mouse too, to start off shift click
1864   // automatically in multiple ... to do this, we'd need to override
1865   // OnOptionSelected and set mStartSelectedIndex if nothing is selected.  Not
1866   // sure of the effects, though, so I'm not doing it just yet.
1867   int32_t startIndex = aStartIndex;
1868   if (startIndex < bottom) {
1869     startIndex = GetSelectedIndex();
1870   }
1871   int32_t newIndex = startIndex + aDoAdjustInc;
1872 
1873   // make sure we start off in the range
1874   if (newIndex < bottom) {
1875     newIndex = 0;
1876   } else if (newIndex >= top) {
1877     newIndex = aNumOptions - 1;
1878   }
1879 
1880   while (1) {
1881     // if the newIndex is selectable, we are golden, bail out
1882     if (IsOptionInteractivelySelectable(newIndex)) {
1883       break;
1884     }
1885 
1886     // it WAS disabled, so sart looking ahead for the next enabled option
1887     newIndex += aDoAdjustIncNext;
1888 
1889     // well, if we reach end reverse the search
1890     if (newIndex < bottom) {
1891       if (doingReverse) {
1892         return;  // if we are in reverse mode and reach the end bail out
1893       } else {
1894         // reset the newIndex to the end of the list we hit
1895         // reverse the incrementer
1896         // set the other end of the list to our original starting index
1897         newIndex = bottom;
1898         aDoAdjustIncNext = 1;
1899         doingReverse = true;
1900         top = startIndex;
1901       }
1902     } else if (newIndex >= top) {
1903       if (doingReverse) {
1904         return;  // if we are in reverse mode and reach the end bail out
1905       } else {
1906         // reset the newIndex to the end of the list we hit
1907         // reverse the incrementer
1908         // set the other end of the list to our original starting index
1909         newIndex = top - 1;
1910         aDoAdjustIncNext = -1;
1911         doingReverse = true;
1912         bottom = startIndex;
1913       }
1914     }
1915   }
1916 
1917   // Looks like we found one
1918   aNewIndex = newIndex;
1919 }
1920 
GetIncrementalString()1921 nsAString& nsListControlFrame::GetIncrementalString() {
1922   if (sIncrementalString == nullptr) sIncrementalString = new nsString();
1923 
1924   return *sIncrementalString;
1925 }
1926 
Shutdown()1927 void nsListControlFrame::Shutdown() {
1928   delete sIncrementalString;
1929   sIncrementalString = nullptr;
1930 }
1931 
DropDownToggleKey(dom::Event * aKeyEvent)1932 void nsListControlFrame::DropDownToggleKey(dom::Event* aKeyEvent) {
1933   // Cocoa widgets do native popups, so don't try to show
1934   // dropdowns there.
1935   if (IsInDropDownMode() && !nsComboboxControlFrame::ToolkitHasNativePopup()) {
1936     aKeyEvent->PreventDefault();
1937     if (!mComboboxFrame->IsDroppedDown()) {
1938       if (!FireShowDropDownEvent(mContent, true, false)) {
1939         mComboboxFrame->ShowDropDown(true);
1940       }
1941     } else {
1942       AutoWeakFrame weakFrame(this);
1943       // mEndSelectionIndex is the last item that got selected.
1944       ComboboxFinish(mEndSelectionIndex);
1945       if (weakFrame.IsAlive()) {
1946         FireOnInputAndOnChange();
1947       }
1948     }
1949   }
1950 }
1951 
KeyDown(dom::Event * aKeyEvent)1952 nsresult nsListControlFrame::KeyDown(dom::Event* aKeyEvent) {
1953   MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
1954 
1955   EventStates eventStates = mContent->AsElement()->State();
1956   if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
1957     return NS_OK;
1958   }
1959 
1960   AutoIncrementalSearchResetter incrementalSearchResetter;
1961 
1962   if (aKeyEvent->DefaultPrevented()) {
1963     return NS_OK;
1964   }
1965 
1966   const WidgetKeyboardEvent* keyEvent =
1967       aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
1968   MOZ_ASSERT(keyEvent,
1969              "DOM event must have WidgetKeyboardEvent for its internal event");
1970 
1971   bool dropDownMenuOnUpDown;
1972   bool dropDownMenuOnSpace;
1973 #ifdef XP_MACOSX
1974   dropDownMenuOnUpDown = IsInDropDownMode() && !mComboboxFrame->IsDroppedDown();
1975   dropDownMenuOnSpace =
1976       !keyEvent->IsAlt() && !keyEvent->IsControl() && !keyEvent->IsMeta();
1977 #else
1978   dropDownMenuOnUpDown = keyEvent->IsAlt();
1979   dropDownMenuOnSpace = IsInDropDownMode() && !mComboboxFrame->IsDroppedDown();
1980 #endif
1981   bool withinIncrementalSearchTime =
1982       keyEvent->mTime - gLastKeyTime <=
1983       StaticPrefs::ui_menu_incremental_search_timeout();
1984   if ((dropDownMenuOnUpDown &&
1985        (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_DOWN)) ||
1986       (dropDownMenuOnSpace && keyEvent->mKeyCode == NS_VK_SPACE &&
1987        !withinIncrementalSearchTime)) {
1988     DropDownToggleKey(aKeyEvent);
1989     if (keyEvent->DefaultPrevented()) {
1990       return NS_OK;
1991     }
1992   }
1993   if (keyEvent->IsAlt()) {
1994     return NS_OK;
1995   }
1996 
1997   // now make sure there are options or we are wasting our time
1998   RefPtr<dom::HTMLOptionsCollection> options = GetOptions();
1999   NS_ENSURE_TRUE(options, NS_ERROR_FAILURE);
2000 
2001   uint32_t numOptions = options->Length();
2002 
2003   // this is the new index to set
2004   int32_t newIndex = kNothingSelected;
2005 
2006   bool isControlOrMeta = (keyEvent->IsControl() || keyEvent->IsMeta());
2007   // Don't try to handle multiple-select pgUp/pgDown in single-select lists.
2008   if (isControlOrMeta && !GetMultiple() &&
2009       (keyEvent->mKeyCode == NS_VK_PAGE_UP ||
2010        keyEvent->mKeyCode == NS_VK_PAGE_DOWN)) {
2011     return NS_OK;
2012   }
2013   if (isControlOrMeta &&
2014       (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_LEFT ||
2015        keyEvent->mKeyCode == NS_VK_DOWN || keyEvent->mKeyCode == NS_VK_RIGHT ||
2016        keyEvent->mKeyCode == NS_VK_HOME || keyEvent->mKeyCode == NS_VK_END)) {
2017     // Don't go into multiple-select mode unless this list can handle it.
2018     isControlOrMeta = mControlSelectMode = GetMultiple();
2019   } else if (keyEvent->mKeyCode != NS_VK_SPACE) {
2020     mControlSelectMode = false;
2021   }
2022 
2023   // We should not change the selection if the popup is "opened
2024   // in the parent process" (even when we're in single-process mode).
2025   bool shouldSelectByKey =
2026       !mComboboxFrame || !mComboboxFrame->IsOpenInParentProcess();
2027 
2028   switch (keyEvent->mKeyCode) {
2029     case NS_VK_UP:
2030     case NS_VK_LEFT:
2031       if (shouldSelectByKey) {
2032         AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
2033                                   static_cast<int32_t>(numOptions), -1, -1);
2034       }
2035       break;
2036     case NS_VK_DOWN:
2037     case NS_VK_RIGHT:
2038       if (shouldSelectByKey) {
2039         AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
2040                                   static_cast<int32_t>(numOptions), 1, 1);
2041       }
2042       break;
2043     case NS_VK_RETURN:
2044       if (IsInDropDownMode()) {
2045         if (mComboboxFrame->IsDroppedDown()) {
2046           // If the select element is a dropdown style, Enter key should be
2047           // consumed while the dropdown is open for security.
2048           aKeyEvent->PreventDefault();
2049 
2050           AutoWeakFrame weakFrame(this);
2051           ComboboxFinish(mEndSelectionIndex);
2052           if (!weakFrame.IsAlive()) {
2053             return NS_OK;
2054           }
2055         }
2056         FireOnInputAndOnChange();
2057         return NS_OK;
2058       }
2059 
2060       // If this is single select listbox, Enter key doesn't cause anything.
2061       if (!GetMultiple()) {
2062         return NS_OK;
2063       }
2064 
2065       newIndex = mEndSelectionIndex;
2066       break;
2067     case NS_VK_ESCAPE: {
2068       // If the select element is a listbox style, Escape key causes nothing.
2069       if (!IsInDropDownMode()) {
2070         return NS_OK;
2071       }
2072 
2073       AboutToRollup();
2074       // If the select element is a dropdown style, Enter key should be
2075       // consumed everytime since Escape key may be pressed accidentally after
2076       // the dropdown is closed by Escepe key.
2077       aKeyEvent->PreventDefault();
2078       return NS_OK;
2079     }
2080     case NS_VK_PAGE_UP: {
2081       if (shouldSelectByKey) {
2082         int32_t itemsPerPage =
2083             std::max(1, static_cast<int32_t>(mNumDisplayRows - 1));
2084         AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
2085                                   static_cast<int32_t>(numOptions),
2086                                   -itemsPerPage, -1);
2087       }
2088       break;
2089     }
2090     case NS_VK_PAGE_DOWN: {
2091       if (shouldSelectByKey) {
2092         int32_t itemsPerPage =
2093             std::max(1, static_cast<int32_t>(mNumDisplayRows - 1));
2094         AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
2095                                   static_cast<int32_t>(numOptions),
2096                                   itemsPerPage, 1);
2097       }
2098       break;
2099     }
2100     case NS_VK_HOME:
2101       if (shouldSelectByKey) {
2102         AdjustIndexForDisabledOpt(0, newIndex, static_cast<int32_t>(numOptions),
2103                                   0, 1);
2104       }
2105       break;
2106     case NS_VK_END:
2107       if (shouldSelectByKey) {
2108         AdjustIndexForDisabledOpt(static_cast<int32_t>(numOptions) - 1,
2109                                   newIndex, static_cast<int32_t>(numOptions), 0,
2110                                   -1);
2111       }
2112       break;
2113 
2114 #if defined(XP_WIN)
2115     case NS_VK_F4:
2116       if (!isControlOrMeta) {
2117         DropDownToggleKey(aKeyEvent);
2118       }
2119       return NS_OK;
2120 #endif
2121 
2122     default:  // printable key will be handled by keypress event.
2123       incrementalSearchResetter.Cancel();
2124       return NS_OK;
2125   }
2126 
2127   aKeyEvent->PreventDefault();
2128 
2129   // Actually process the new index and let the selection code
2130   // do the scrolling for us
2131   PostHandleKeyEvent(newIndex, 0, keyEvent->IsShift(), isControlOrMeta);
2132   return NS_OK;
2133 }
2134 
KeyPress(dom::Event * aKeyEvent)2135 nsresult nsListControlFrame::KeyPress(dom::Event* aKeyEvent) {
2136   MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
2137 
2138   EventStates eventStates = mContent->AsElement()->State();
2139   if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
2140     return NS_OK;
2141   }
2142 
2143   AutoIncrementalSearchResetter incrementalSearchResetter;
2144 
2145   const WidgetKeyboardEvent* keyEvent =
2146       aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
2147   MOZ_ASSERT(keyEvent,
2148              "DOM event must have WidgetKeyboardEvent for its internal event");
2149 
2150   // Select option with this as the first character
2151   // XXX Not I18N compliant
2152 
2153   // Don't do incremental search if the key event has already consumed.
2154   if (keyEvent->DefaultPrevented()) {
2155     return NS_OK;
2156   }
2157 
2158   if (keyEvent->IsAlt()) {
2159     return NS_OK;
2160   }
2161 
2162   // With some keyboard layout, space key causes non-ASCII space.
2163   // So, the check in keydown event handler isn't enough, we need to check it
2164   // again with keypress event.
2165   if (keyEvent->mCharCode != ' ') {
2166     mControlSelectMode = false;
2167   }
2168 
2169   bool isControlOrMeta = (keyEvent->IsControl() || keyEvent->IsMeta());
2170   if (isControlOrMeta && keyEvent->mCharCode != ' ') {
2171     return NS_OK;
2172   }
2173 
2174   // NOTE: If mKeyCode of keypress event is not 0, mCharCode is always 0.
2175   //       Therefore, all non-printable keys are not handled after this block.
2176   if (!keyEvent->mCharCode) {
2177     // Backspace key will delete the last char in the string.  Otherwise,
2178     // non-printable keypress should reset incremental search.
2179     if (keyEvent->mKeyCode == NS_VK_BACK) {
2180       incrementalSearchResetter.Cancel();
2181       if (!GetIncrementalString().IsEmpty()) {
2182         GetIncrementalString().Truncate(GetIncrementalString().Length() - 1);
2183       }
2184       aKeyEvent->PreventDefault();
2185     } else {
2186       // XXX When a select element has focus, even if the key causes nothing,
2187       //     it might be better to call preventDefault() here because nobody
2188       //     should expect one of other elements including chrome handles the
2189       //     key event.
2190     }
2191     return NS_OK;
2192   }
2193 
2194   incrementalSearchResetter.Cancel();
2195 
2196   // We ate the key if we got this far.
2197   aKeyEvent->PreventDefault();
2198 
2199   // XXX Why don't we check/modify timestamp first?
2200 
2201   // Incremental Search: if time elapsed is below
2202   // ui.menu.incremental_search.timeout, append this keystroke to the search
2203   // string we will use to find options and start searching at the current
2204   // keystroke.  Otherwise, Truncate the string if it's been a long time
2205   // since our last keypress.
2206   if (keyEvent->mTime - gLastKeyTime >
2207       StaticPrefs::ui_menu_incremental_search_timeout()) {
2208     // If this is ' ' and we are at the beginning of the string, treat it as
2209     // "select this option" (bug 191543)
2210     if (keyEvent->mCharCode == ' ') {
2211       // Actually process the new index and let the selection code
2212       // do the scrolling for us
2213       PostHandleKeyEvent(mEndSelectionIndex, keyEvent->mCharCode,
2214                          keyEvent->IsShift(), isControlOrMeta);
2215 
2216       return NS_OK;
2217     }
2218 
2219     GetIncrementalString().Truncate();
2220   }
2221 
2222   gLastKeyTime = keyEvent->mTime;
2223 
2224   // Append this keystroke to the search string.
2225   char16_t uniChar = ToLowerCase(static_cast<char16_t>(keyEvent->mCharCode));
2226   GetIncrementalString().Append(uniChar);
2227 
2228   // See bug 188199, if all letters in incremental string are same, just try to
2229   // match the first one
2230   nsAutoString incrementalString(GetIncrementalString());
2231   uint32_t charIndex = 1, stringLength = incrementalString.Length();
2232   while (charIndex < stringLength &&
2233          incrementalString[charIndex] == incrementalString[charIndex - 1]) {
2234     charIndex++;
2235   }
2236   if (charIndex == stringLength) {
2237     incrementalString.Truncate(1);
2238     stringLength = 1;
2239   }
2240 
2241   // Determine where we're going to start reading the string
2242   // If we have multiple characters to look for, we start looking *at* the
2243   // current option.  If we have only one character to look for, we start
2244   // looking *after* the current option.
2245   // Exception: if there is no option selected to start at, we always start
2246   // *at* 0.
2247   int32_t startIndex = GetSelectedIndex();
2248   if (startIndex == kNothingSelected) {
2249     startIndex = 0;
2250   } else if (stringLength == 1) {
2251     startIndex++;
2252   }
2253 
2254   // now make sure there are options or we are wasting our time
2255   RefPtr<dom::HTMLOptionsCollection> options = GetOptions();
2256   NS_ENSURE_TRUE(options, NS_ERROR_FAILURE);
2257 
2258   uint32_t numOptions = options->Length();
2259 
2260   AutoWeakFrame weakFrame(this);
2261   for (uint32_t i = 0; i < numOptions; ++i) {
2262     uint32_t index = (i + startIndex) % numOptions;
2263     RefPtr<dom::HTMLOptionElement> optionElement = options->ItemAsOption(index);
2264     if (!optionElement || !optionElement->GetPrimaryFrame()) {
2265       continue;
2266     }
2267 
2268     nsAutoString text;
2269     optionElement->GetRenderedLabel(text);
2270     if (!StringBeginsWith(
2271             nsContentUtils::TrimWhitespace<
2272                 nsContentUtils::IsHTMLWhitespaceOrNBSP>(text, false),
2273             incrementalString, nsCaseInsensitiveStringComparator)) {
2274       continue;
2275     }
2276 
2277     bool wasChanged =
2278         PerformSelection(index, keyEvent->IsShift(), isControlOrMeta);
2279     if (!weakFrame.IsAlive()) {
2280       return NS_OK;
2281     }
2282     if (!wasChanged) {
2283       break;
2284     }
2285 
2286     // If UpdateSelection() returns false, that means the frame is no longer
2287     // alive. We should stop doing anything.
2288     if (!UpdateSelection()) {
2289       return NS_OK;
2290     }
2291     break;
2292   }
2293 
2294   return NS_OK;
2295 }
2296 
PostHandleKeyEvent(int32_t aNewIndex,uint32_t aCharCode,bool aIsShift,bool aIsControlOrMeta)2297 void nsListControlFrame::PostHandleKeyEvent(int32_t aNewIndex,
2298                                             uint32_t aCharCode, bool aIsShift,
2299                                             bool aIsControlOrMeta) {
2300   if (aNewIndex == kNothingSelected) {
2301     int32_t focusedIndex = mEndSelectionIndex == kNothingSelected
2302                                ? GetSelectedIndex()
2303                                : mEndSelectionIndex;
2304     if (focusedIndex != kNothingSelected) {
2305       return;
2306     }
2307     // No options are selected.  In this case the focus ring is on the first
2308     // non-disabled option (if any), so we should behave as if that's the option
2309     // the user acted on.
2310     if (!GetNonDisabledOptionFrom(0, &aNewIndex)) {
2311       return;
2312     }
2313   }
2314 
2315   // If you hold control, but not shift, no key will actually do anything
2316   // except space.
2317   AutoWeakFrame weakFrame(this);
2318   bool wasChanged = false;
2319   if (aIsControlOrMeta && !aIsShift && aCharCode != ' ') {
2320     mStartSelectionIndex = aNewIndex;
2321     mEndSelectionIndex = aNewIndex;
2322     InvalidateFocus();
2323     ScrollToIndex(aNewIndex);
2324     if (!weakFrame.IsAlive()) {
2325       return;
2326     }
2327 
2328 #ifdef ACCESSIBILITY
2329     FireMenuItemActiveEvent();
2330 #endif
2331   } else if (mControlSelectMode && aCharCode == ' ') {
2332     wasChanged = SingleSelection(aNewIndex, true);
2333   } else {
2334     wasChanged = PerformSelection(aNewIndex, aIsShift, aIsControlOrMeta);
2335   }
2336   if (wasChanged && weakFrame.IsAlive()) {
2337     // dispatch event, update combobox, etc.
2338     UpdateSelection();
2339   }
2340 }
2341 
2342 /******************************************************************************
2343  * nsListEventListener
2344  *****************************************************************************/
2345 
NS_IMPL_ISUPPORTS(nsListEventListener,nsIDOMEventListener)2346 NS_IMPL_ISUPPORTS(nsListEventListener, nsIDOMEventListener)
2347 
2348 NS_IMETHODIMP
2349 nsListEventListener::HandleEvent(dom::Event* aEvent) {
2350   if (!mFrame) return NS_OK;
2351 
2352   nsAutoString eventType;
2353   aEvent->GetType(eventType);
2354   if (eventType.EqualsLiteral("keydown")) {
2355     return mFrame->nsListControlFrame::KeyDown(aEvent);
2356   }
2357   if (eventType.EqualsLiteral("keypress")) {
2358     return mFrame->nsListControlFrame::KeyPress(aEvent);
2359   }
2360   if (eventType.EqualsLiteral("mousedown")) {
2361     if (aEvent->DefaultPrevented()) {
2362       return NS_OK;
2363     }
2364     return mFrame->nsListControlFrame::MouseDown(aEvent);
2365   }
2366   if (eventType.EqualsLiteral("mouseup")) {
2367     // Don't try to honor defaultPrevented here - it's not web compatible.
2368     // (bug 1194733)
2369     return mFrame->nsListControlFrame::MouseUp(aEvent);
2370   }
2371   if (eventType.EqualsLiteral("mousemove")) {
2372     // I don't think we want to honor defaultPrevented on mousemove
2373     // in general, and it would only prevent highlighting here.
2374     return mFrame->nsListControlFrame::MouseMove(aEvent);
2375   }
2376 
2377   MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
2378   return NS_OK;
2379 }
2380