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