Backout a74bd5095902, Bug 959405 - Please update the Buri Moz-central, 1.3, 1.2 with...
[gecko.git] / layout / forms / nsListControlFrame.cpp
blob307a8a75905d1b77cf49ec2d3fe7b4de36728683
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "nscore.h"
7 #include "nsCOMPtr.h"
8 #include "nsUnicharUtils.h"
9 #include "nsListControlFrame.h"
10 #include "nsFormControlFrame.h" // for COMPARE macro
11 #include "nsGkAtoms.h"
12 #include "nsIDOMHTMLSelectElement.h"
13 #include "nsIDOMHTMLOptionElement.h"
14 #include "nsComboboxControlFrame.h"
15 #include "nsIDOMHTMLOptGroupElement.h"
16 #include "nsIPresShell.h"
17 #include "nsEventStateManager.h"
18 #include "nsIDOMMouseEvent.h"
19 #include "nsIXULRuntime.h"
20 #include "nsFontMetrics.h"
21 #include "nsIScrollableFrame.h"
22 #include "nsCSSRendering.h"
23 #include "nsIDOMEventListener.h"
24 #include "nsLayoutUtils.h"
25 #include "nsDisplayList.h"
26 #include "nsContentUtils.h"
27 #include "mozilla/Attributes.h"
28 #include "mozilla/dom/HTMLOptionsCollection.h"
29 #include "mozilla/dom/HTMLSelectElement.h"
30 #include "mozilla/LookAndFeel.h"
31 #include "mozilla/MouseEvents.h"
32 #include "mozilla/Preferences.h"
33 #include "mozilla/TextEvents.h"
34 #include <algorithm>
36 using namespace mozilla;
38 // Constants
39 const uint32_t kMaxDropDownRows = 20; // This matches the setting for 4.x browsers
40 const int32_t kNothingSelected = -1;
42 // Static members
43 nsListControlFrame * nsListControlFrame::mFocused = nullptr;
44 nsString * nsListControlFrame::sIncrementalString = nullptr;
46 // Using for incremental typing navigation
47 #define INCREMENTAL_SEARCH_KEYPRESS_TIME 1000
48 // XXX, kyle.yuan@sun.com, there are 4 definitions for the same purpose:
49 // nsMenuPopupFrame.h, nsListControlFrame.cpp, listbox.xml, tree.xml
50 // need to find a good place to put them together.
51 // if someone changes one, please also change the other.
53 DOMTimeStamp nsListControlFrame::gLastKeyTime = 0;
55 /******************************************************************************
56 * nsListEventListener
57 * This class is responsible for propagating events to the nsListControlFrame.
58 * Frames are not refcounted so they can't be used as event listeners.
59 *****************************************************************************/
61 class nsListEventListener MOZ_FINAL : public nsIDOMEventListener
63 public:
64 nsListEventListener(nsListControlFrame *aFrame)
65 : mFrame(aFrame) { }
67 void SetFrame(nsListControlFrame *aFrame) { mFrame = aFrame; }
69 NS_DECL_ISUPPORTS
70 NS_DECL_NSIDOMEVENTLISTENER
72 private:
73 nsListControlFrame *mFrame;
76 //---------------------------------------------------------
77 nsIFrame*
78 NS_NewListControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
80 nsListControlFrame* it =
81 new (aPresShell) nsListControlFrame(aPresShell, aPresShell->GetDocument(), aContext);
83 it->AddStateBits(NS_FRAME_INDEPENDENT_SELECTION);
85 return it;
88 NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame)
90 //---------------------------------------------------------
91 nsListControlFrame::nsListControlFrame(
92 nsIPresShell* aShell, nsIDocument* aDocument, nsStyleContext* aContext)
93 : nsHTMLScrollFrame(aShell, aContext, false),
94 mMightNeedSecondPass(false),
95 mHasPendingInterruptAtStartOfReflow(false),
96 mDropdownCanGrow(false),
97 mLastDropdownComputedHeight(NS_UNCONSTRAINEDSIZE)
99 mComboboxFrame = nullptr;
100 mChangesSinceDragStart = false;
101 mButtonDown = false;
103 mIsAllContentHere = false;
104 mIsAllFramesHere = false;
105 mHasBeenInitialized = false;
106 mNeedToReset = true;
107 mPostChildrenLoadedReset = false;
109 mControlSelectMode = false;
112 //---------------------------------------------------------
113 nsListControlFrame::~nsListControlFrame()
115 mComboboxFrame = nullptr;
118 // for Bug 47302 (remove this comment later)
119 void
120 nsListControlFrame::DestroyFrom(nsIFrame* aDestructRoot)
122 // get the receiver interface from the browser button's content node
123 ENSURE_TRUE(mContent);
125 // Clear the frame pointer on our event listener, just in case the
126 // event listener can outlive the frame.
128 mEventListener->SetFrame(nullptr);
130 mContent->RemoveSystemEventListener(NS_LITERAL_STRING("keydown"),
131 mEventListener, false);
132 mContent->RemoveSystemEventListener(NS_LITERAL_STRING("keypress"),
133 mEventListener, false);
134 mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mousedown"),
135 mEventListener, false);
136 mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mouseup"),
137 mEventListener, false);
138 mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mousemove"),
139 mEventListener, false);
141 nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false);
142 nsHTMLScrollFrame::DestroyFrom(aDestructRoot);
145 void
146 nsListControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
147 const nsRect& aDirtyRect,
148 const nsDisplayListSet& aLists)
150 // We allow visibility:hidden <select>s to contain visible options.
152 // Don't allow painting of list controls when painting is suppressed.
153 // XXX why do we need this here? we should never reach this. Maybe
154 // because these can have widgets? Hmm
155 if (aBuilder->IsBackgroundOnly())
156 return;
158 DO_GLOBAL_REFLOW_COUNT_DSP("nsListControlFrame");
160 if (IsInDropDownMode()) {
161 NS_ASSERTION(NS_GET_A(mLastDropdownBackstopColor) == 255,
162 "need an opaque backstop color");
163 // XXX Because we have an opaque widget and we get called to paint with
164 // this frame as the root of a stacking context we need make sure to draw
165 // some opaque color over the whole widget. (Bug 511323)
166 aLists.BorderBackground()->AppendNewToBottom(
167 new (aBuilder) nsDisplaySolidColor(aBuilder,
168 this, nsRect(aBuilder->ToReferenceFrame(this), GetSize()),
169 mLastDropdownBackstopColor));
172 nsHTMLScrollFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists);
176 * This is called by the SelectsAreaFrame, which is the same
177 * as the frame returned by GetOptionsContainer. It's the frame which is
178 * scrolled by us.
179 * @param aPt the offset of this frame, relative to the rendering reference
180 * frame
182 void nsListControlFrame::PaintFocus(nsRenderingContext& aRC, nsPoint aPt)
184 if (mFocused != this) return;
186 nsPresContext* presContext = PresContext();
188 nsIFrame* containerFrame = GetOptionsContainer();
189 if (!containerFrame) return;
191 nsIFrame* childframe = nullptr;
192 nsCOMPtr<nsIContent> focusedContent = GetCurrentOption();
193 if (focusedContent) {
194 childframe = focusedContent->GetPrimaryFrame();
197 nsRect fRect;
198 if (childframe) {
199 // get the child rect
200 fRect = childframe->GetRect();
201 // get it into our coordinates
202 fRect.MoveBy(childframe->GetParent()->GetOffsetTo(this));
203 } else {
204 float inflation = nsLayoutUtils::FontSizeInflationFor(this);
205 fRect.x = fRect.y = 0;
206 fRect.width = GetScrollPortRect().width;
207 fRect.height = CalcFallbackRowHeight(inflation);
208 fRect.MoveBy(containerFrame->GetOffsetTo(this));
210 fRect += aPt;
212 bool lastItemIsSelected = false;
213 if (focusedContent) {
214 nsCOMPtr<nsIDOMHTMLOptionElement> domOpt =
215 do_QueryInterface(focusedContent);
216 if (domOpt) {
217 domOpt->GetSelected(&lastItemIsSelected);
221 // set up back stop colors and then ask L&F service for the real colors
222 nscolor color =
223 LookAndFeel::GetColor(lastItemIsSelected ?
224 LookAndFeel::eColorID_WidgetSelectForeground :
225 LookAndFeel::eColorID_WidgetSelectBackground);
227 nsCSSRendering::PaintFocus(presContext, aRC, fRect, color);
230 void
231 nsListControlFrame::InvalidateFocus()
233 if (mFocused != this)
234 return;
236 nsIFrame* containerFrame = GetOptionsContainer();
237 if (containerFrame) {
238 containerFrame->InvalidateFrame();
242 NS_QUERYFRAME_HEAD(nsListControlFrame)
243 NS_QUERYFRAME_ENTRY(nsIFormControlFrame)
244 NS_QUERYFRAME_ENTRY(nsIListControlFrame)
245 NS_QUERYFRAME_ENTRY(nsISelectControlFrame)
246 NS_QUERYFRAME_TAIL_INHERITING(nsHTMLScrollFrame)
248 #ifdef ACCESSIBILITY
249 a11y::AccType
250 nsListControlFrame::AccessibleType()
252 return a11y::eHTMLSelectListType;
254 #endif
256 static nscoord
257 GetMaxOptionHeight(nsIFrame* aContainer)
259 nscoord result = 0;
260 for (nsIFrame* option = aContainer->GetFirstPrincipalChild();
261 option; option = option->GetNextSibling()) {
262 nscoord optionHeight;
263 if (nsCOMPtr<nsIDOMHTMLOptGroupElement>
264 (do_QueryInterface(option->GetContent()))) {
265 // an optgroup
266 optionHeight = GetMaxOptionHeight(option);
267 } else {
268 // an option
269 optionHeight = option->GetSize().height;
271 if (result < optionHeight)
272 result = optionHeight;
274 return result;
277 //-----------------------------------------------------------------
278 // Main Reflow for ListBox/Dropdown
279 //-----------------------------------------------------------------
281 nscoord
282 nsListControlFrame::CalcHeightOfARow()
284 // Calculate the height of a single row in the listbox or dropdown list by
285 // using the tallest thing in the subtree, since there may be option groups
286 // in addition to option elements, either of which may be visible or
287 // invisible, may use different fonts, etc.
288 int32_t heightOfARow = GetMaxOptionHeight(GetOptionsContainer());
290 // Check to see if we have zero items (and optimize by checking
291 // heightOfARow first)
292 if (heightOfARow == 0 && GetNumberOfOptions() == 0) {
293 float inflation = nsLayoutUtils::FontSizeInflationFor(this);
294 heightOfARow = CalcFallbackRowHeight(inflation);
297 return heightOfARow;
300 nscoord
301 nsListControlFrame::GetPrefWidth(nsRenderingContext *aRenderingContext)
303 nscoord result;
304 DISPLAY_PREF_WIDTH(this, result);
306 // Always add scrollbar widths to the pref-width of the scrolled
307 // content. Combobox frames depend on this happening in the dropdown,
308 // and standalone listboxes are overflow:scroll so they need it too.
309 result = GetScrolledFrame()->GetPrefWidth(aRenderingContext);
310 result = NSCoordSaturatingAdd(result,
311 GetDesiredScrollbarSizes(PresContext(), aRenderingContext).LeftRight());
313 return result;
316 nscoord
317 nsListControlFrame::GetMinWidth(nsRenderingContext *aRenderingContext)
319 nscoord result;
320 DISPLAY_MIN_WIDTH(this, result);
322 // Always add scrollbar widths to the min-width of the scrolled
323 // content. Combobox frames depend on this happening in the dropdown,
324 // and standalone listboxes are overflow:scroll so they need it too.
325 result = GetScrolledFrame()->GetMinWidth(aRenderingContext);
326 result += GetDesiredScrollbarSizes(PresContext(), aRenderingContext).LeftRight();
328 return result;
331 NS_IMETHODIMP
332 nsListControlFrame::Reflow(nsPresContext* aPresContext,
333 nsHTMLReflowMetrics& aDesiredSize,
334 const nsHTMLReflowState& aReflowState,
335 nsReflowStatus& aStatus)
337 NS_PRECONDITION(aReflowState.ComputedWidth() != NS_UNCONSTRAINEDSIZE,
338 "Must have a computed width");
340 SchedulePaint();
342 mHasPendingInterruptAtStartOfReflow = aPresContext->HasPendingInterrupt();
344 // If all the content and frames are here
345 // then initialize it before reflow
346 if (mIsAllContentHere && !mHasBeenInitialized) {
347 if (false == mIsAllFramesHere) {
348 CheckIfAllFramesHere();
350 if (mIsAllFramesHere && !mHasBeenInitialized) {
351 mHasBeenInitialized = true;
355 if (GetStateBits() & NS_FRAME_FIRST_REFLOW) {
356 nsFormControlFrame::RegUnRegAccessKey(this, true);
359 if (IsInDropDownMode()) {
360 return ReflowAsDropdown(aPresContext, aDesiredSize, aReflowState, aStatus);
364 * Due to the fact that our intrinsic height depends on the heights of our
365 * kids, we end up having to do two-pass reflow, in general -- the first pass
366 * to find the intrinsic height and a second pass to reflow the scrollframe
367 * at that height (which will size the scrollbars correctly, etc).
369 * Naturaly, we want to avoid doing the second reflow as much as possible.
370 * We can skip it in the following cases (in all of which the first reflow is
371 * already happening at the right height):
373 * - We're reflowing with a constrained computed height -- just use that
374 * height.
375 * - We're not dirty and have no dirty kids and shouldn't be reflowing all
376 * kids. In this case, our cached max height of a child is not going to
377 * change.
378 * - We do our first reflow using our cached max height of a child, then
379 * compute the new max height and it's the same as the old one.
382 bool autoHeight = (aReflowState.ComputedHeight() == NS_UNCONSTRAINEDSIZE);
384 mMightNeedSecondPass = autoHeight &&
385 (NS_SUBTREE_DIRTY(this) || aReflowState.ShouldReflowAllKids());
387 nsHTMLReflowState state(aReflowState);
388 int32_t length = GetNumberOfRows();
390 nscoord oldHeightOfARow = HeightOfARow();
392 if (!(GetStateBits() & NS_FRAME_FIRST_REFLOW) && autoHeight) {
393 // When not doing an initial reflow, and when the height is auto, start off
394 // with our computed height set to what we'd expect our height to be.
395 nscoord computedHeight = CalcIntrinsicHeight(oldHeightOfARow, length);
396 computedHeight = state.ApplyMinMaxHeight(computedHeight);
397 state.SetComputedHeight(computedHeight);
400 nsresult rv = nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize,
401 state, aStatus);
402 NS_ENSURE_SUCCESS(rv, rv);
404 if (!mMightNeedSecondPass) {
405 NS_ASSERTION(!autoHeight || HeightOfARow() == oldHeightOfARow,
406 "How did our height of a row change if nothing was dirty?");
407 NS_ASSERTION(!autoHeight ||
408 !(GetStateBits() & NS_FRAME_FIRST_REFLOW),
409 "How do we not need a second pass during initial reflow at "
410 "auto height?");
411 NS_ASSERTION(!IsScrollbarUpdateSuppressed(),
412 "Shouldn't be suppressing if we don't need a second pass!");
413 if (!autoHeight) {
414 // Update our mNumDisplayRows based on our new row height now that we
415 // know it. Note that if autoHeight and we landed in this code then we
416 // already set mNumDisplayRows in CalcIntrinsicHeight. Also note that we
417 // can't use HeightOfARow() here because that just uses a cached value
418 // that we didn't compute.
419 nscoord rowHeight = CalcHeightOfARow();
420 if (rowHeight == 0) {
421 // Just pick something
422 mNumDisplayRows = 1;
423 } else {
424 mNumDisplayRows = std::max(1, state.ComputedHeight() / rowHeight);
428 return rv;
431 mMightNeedSecondPass = false;
433 // Now see whether we need a second pass. If we do, our nsSelectsAreaFrame
434 // will have suppressed the scrollbar update.
435 if (!IsScrollbarUpdateSuppressed()) {
436 // All done. No need to do more reflow.
437 NS_ASSERTION(!IsScrollbarUpdateSuppressed(),
438 "Shouldn't be suppressing if the height of a row has not "
439 "changed!");
440 return rv;
443 SetSuppressScrollbarUpdate(false);
445 // Gotta reflow again.
446 // XXXbz We're just changing the height here; do we need to dirty ourselves
447 // or anything like that? We might need to, per the letter of the reflow
448 // protocol, but things seem to work fine without it... Is that just an
449 // implementation detail of nsHTMLScrollFrame that we're depending on?
450 nsHTMLScrollFrame::DidReflow(aPresContext, &state,
451 nsDidReflowStatus::FINISHED);
453 // Now compute the height we want to have
454 nscoord computedHeight = CalcIntrinsicHeight(HeightOfARow(), length);
455 computedHeight = state.ApplyMinMaxHeight(computedHeight);
456 state.SetComputedHeight(computedHeight);
458 nsHTMLScrollFrame::WillReflow(aPresContext);
460 // XXXbz to make the ascent really correct, we should add our
461 // mComputedPadding.top to it (and subtract it from descent). Need that
462 // because nsGfxScrollFrame just adds in the border....
463 return nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
466 nsresult
467 nsListControlFrame::ReflowAsDropdown(nsPresContext* aPresContext,
468 nsHTMLReflowMetrics& aDesiredSize,
469 const nsHTMLReflowState& aReflowState,
470 nsReflowStatus& aStatus)
472 NS_PRECONDITION(aReflowState.ComputedHeight() == NS_UNCONSTRAINEDSIZE,
473 "We should not have a computed height here!");
475 mMightNeedSecondPass = NS_SUBTREE_DIRTY(this) ||
476 aReflowState.ShouldReflowAllKids();
478 #ifdef DEBUG
479 nscoord oldHeightOfARow = HeightOfARow();
480 nscoord oldVisibleHeight = (GetStateBits() & NS_FRAME_FIRST_REFLOW) ?
481 NS_UNCONSTRAINEDSIZE : GetScrolledFrame()->GetSize().height;
482 #endif
484 nsHTMLReflowState state(aReflowState);
486 if (!(GetStateBits() & NS_FRAME_FIRST_REFLOW)) {
487 // When not doing an initial reflow, and when the height is auto, start off
488 // with our computed height set to what we'd expect our height to be.
489 // Note: At this point, mLastDropdownComputedHeight can be
490 // NS_UNCONSTRAINEDSIZE in cases when last time we didn't have to constrain
491 // the height. That's fine; just do the same thing as last time.
492 state.SetComputedHeight(mLastDropdownComputedHeight);
495 nsresult rv = nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize,
496 state, aStatus);
497 NS_ENSURE_SUCCESS(rv, rv);
499 if (!mMightNeedSecondPass) {
500 NS_ASSERTION(oldVisibleHeight == GetScrolledFrame()->GetSize().height,
501 "How did our kid's height change if nothing was dirty?");
502 NS_ASSERTION(HeightOfARow() == oldHeightOfARow,
503 "How did our height of a row change if nothing was dirty?");
504 NS_ASSERTION(!IsScrollbarUpdateSuppressed(),
505 "Shouldn't be suppressing if we don't need a second pass!");
506 NS_ASSERTION(!(GetStateBits() & NS_FRAME_FIRST_REFLOW),
507 "How can we avoid a second pass during first reflow?");
508 return rv;
511 mMightNeedSecondPass = false;
513 // Now see whether we need a second pass. If we do, our nsSelectsAreaFrame
514 // will have suppressed the scrollbar update.
515 if (!IsScrollbarUpdateSuppressed()) {
516 // All done. No need to do more reflow.
517 NS_ASSERTION(!(GetStateBits() & NS_FRAME_FIRST_REFLOW),
518 "How can we avoid a second pass during first reflow?");
519 return rv;
522 SetSuppressScrollbarUpdate(false);
524 nscoord visibleHeight = GetScrolledFrame()->GetSize().height;
525 nscoord heightOfARow = HeightOfARow();
527 // Gotta reflow again.
528 // XXXbz We're just changing the height here; do we need to dirty ourselves
529 // or anything like that? We might need to, per the letter of the reflow
530 // protocol, but things seem to work fine without it... Is that just an
531 // implementation detail of nsHTMLScrollFrame that we're depending on?
532 nsHTMLScrollFrame::DidReflow(aPresContext, &state,
533 nsDidReflowStatus::FINISHED);
535 // Now compute the height we want to have.
536 // Note: no need to apply min/max constraints, since we have no such
537 // rules applied to the combobox dropdown.
539 mDropdownCanGrow = false;
540 if (visibleHeight <= 0 || heightOfARow <= 0) {
541 // Looks like we have no options. Just size us to a single row height.
542 state.SetComputedHeight(heightOfARow);
543 mNumDisplayRows = 1;
544 } else {
545 nsComboboxControlFrame* combobox = static_cast<nsComboboxControlFrame*>(mComboboxFrame);
546 nsPoint translation;
547 nscoord above, below;
548 combobox->GetAvailableDropdownSpace(&above, &below, &translation);
549 if (above <= 0 && below <= 0) {
550 state.SetComputedHeight(heightOfARow);
551 mNumDisplayRows = 1;
552 mDropdownCanGrow = GetNumberOfRows() > 1;
553 } else {
554 nscoord bp = aReflowState.ComputedPhysicalBorderPadding().TopBottom();
555 nscoord availableHeight = std::max(above, below) - bp;
556 nscoord newHeight;
557 uint32_t rows;
558 if (visibleHeight <= availableHeight) {
559 // The dropdown fits in the available height.
560 rows = GetNumberOfRows();
561 mNumDisplayRows = clamped<uint32_t>(rows, 1, kMaxDropDownRows);
562 if (mNumDisplayRows == rows) {
563 newHeight = visibleHeight; // use the exact height
564 } else {
565 newHeight = mNumDisplayRows * heightOfARow; // approximate
567 } else {
568 rows = availableHeight / heightOfARow;
569 mNumDisplayRows = clamped<uint32_t>(rows, 1, kMaxDropDownRows);
570 newHeight = mNumDisplayRows * heightOfARow; // approximate
572 state.SetComputedHeight(newHeight);
573 mDropdownCanGrow = visibleHeight - newHeight >= heightOfARow &&
574 mNumDisplayRows != kMaxDropDownRows;
578 mLastDropdownComputedHeight = state.ComputedHeight();
580 nsHTMLScrollFrame::WillReflow(aPresContext);
581 return nsHTMLScrollFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
584 ScrollbarStyles
585 nsListControlFrame::GetScrollbarStyles() const
587 // We can't express this in the style system yet; when we can, this can go away
588 // and GetScrollbarStyles can be devirtualized
589 int32_t verticalStyle = IsInDropDownMode() ? NS_STYLE_OVERFLOW_AUTO
590 : NS_STYLE_OVERFLOW_SCROLL;
591 return ScrollbarStyles(NS_STYLE_OVERFLOW_HIDDEN, verticalStyle);
594 bool
595 nsListControlFrame::ShouldPropagateComputedHeightToScrolledContent() const
597 return !IsInDropDownMode();
600 //---------------------------------------------------------
601 nsIFrame*
602 nsListControlFrame::GetContentInsertionFrame() {
603 return GetOptionsContainer()->GetContentInsertionFrame();
606 //---------------------------------------------------------
607 bool
608 nsListControlFrame::ExtendedSelection(int32_t aStartIndex,
609 int32_t aEndIndex,
610 bool aClearAll)
612 return SetOptionsSelectedFromFrame(aStartIndex, aEndIndex,
613 true, aClearAll);
616 //---------------------------------------------------------
617 bool
618 nsListControlFrame::SingleSelection(int32_t aClickedIndex, bool aDoToggle)
620 if (mComboboxFrame) {
621 mComboboxFrame->UpdateRecentIndex(GetSelectedIndex());
624 bool wasChanged = false;
625 // Get Current selection
626 if (aDoToggle) {
627 wasChanged = ToggleOptionSelectedFromFrame(aClickedIndex);
628 } else {
629 wasChanged = SetOptionsSelectedFromFrame(aClickedIndex, aClickedIndex,
630 true, true);
632 nsWeakFrame weakFrame(this);
633 ScrollToIndex(aClickedIndex);
634 if (!weakFrame.IsAlive()) {
635 return wasChanged;
638 #ifdef ACCESSIBILITY
639 bool isCurrentOptionChanged = mEndSelectionIndex != aClickedIndex;
640 #endif
641 mStartSelectionIndex = aClickedIndex;
642 mEndSelectionIndex = aClickedIndex;
643 InvalidateFocus();
645 #ifdef ACCESSIBILITY
646 if (isCurrentOptionChanged) {
647 FireMenuItemActiveEvent();
649 #endif
651 return wasChanged;
654 void
655 nsListControlFrame::InitSelectionRange(int32_t aClickedIndex)
658 // If nothing is selected, set the start selection depending on where
659 // the user clicked and what the initial selection is:
660 // - if the user clicked *before* selectedIndex, set the start index to
661 // the end of the first contiguous selection.
662 // - if the user clicked *after* the end of the first contiguous
663 // selection, set the start index to selectedIndex.
664 // - if the user clicked *within* the first contiguous selection, set the
665 // start index to selectedIndex.
666 // The last two rules, of course, boil down to the same thing: if the user
667 // clicked >= selectedIndex, return selectedIndex.
669 // This makes it so that shift click works properly when you first click
670 // in a multiple select.
672 int32_t selectedIndex = GetSelectedIndex();
673 if (selectedIndex >= 0) {
674 // Get the end of the contiguous selection
675 nsRefPtr<dom::HTMLOptionsCollection> options = GetOptions();
676 NS_ASSERTION(options, "Collection of options is null!");
677 uint32_t numOptions = options->Length();
678 // Push i to one past the last selected index in the group.
679 uint32_t i;
680 for (i = selectedIndex + 1; i < numOptions; i++) {
681 if (!options->ItemAsOption(i)->Selected()) {
682 break;
686 if (aClickedIndex < selectedIndex) {
687 // User clicked before selection, so start selection at end of
688 // contiguous selection
689 mStartSelectionIndex = i-1;
690 mEndSelectionIndex = selectedIndex;
691 } else {
692 // User clicked after selection, so start selection at start of
693 // contiguous selection
694 mStartSelectionIndex = selectedIndex;
695 mEndSelectionIndex = i-1;
700 static uint32_t
701 CountOptionsAndOptgroups(nsIFrame* aFrame)
703 uint32_t count = 0;
704 nsFrameList::Enumerator e(aFrame->PrincipalChildList());
705 for (; !e.AtEnd(); e.Next()) {
706 nsIFrame* child = e.get();
707 nsIContent* content = child->GetContent();
708 if (content) {
709 if (content->IsHTML(nsGkAtoms::option)) {
710 ++count;
711 } else {
712 nsCOMPtr<nsIDOMHTMLOptGroupElement> optgroup = do_QueryInterface(content);
713 if (optgroup) {
714 nsAutoString label;
715 optgroup->GetLabel(label);
716 if (label.Length() > 0) {
717 ++count;
719 count += CountOptionsAndOptgroups(child);
724 return count;
727 uint32_t
728 nsListControlFrame::GetNumberOfRows()
730 return ::CountOptionsAndOptgroups(GetContentInsertionFrame());
733 //---------------------------------------------------------
734 bool
735 nsListControlFrame::PerformSelection(int32_t aClickedIndex,
736 bool aIsShift,
737 bool aIsControl)
739 bool wasChanged = false;
741 if (aClickedIndex == kNothingSelected) {
743 else if (GetMultiple()) {
744 if (aIsShift) {
745 // Make sure shift+click actually does something expected when
746 // the user has never clicked on the select
747 if (mStartSelectionIndex == kNothingSelected) {
748 InitSelectionRange(aClickedIndex);
751 // Get the range from beginning (low) to end (high)
752 // Shift *always* works, even if the current option is disabled
753 int32_t startIndex;
754 int32_t endIndex;
755 if (mStartSelectionIndex == kNothingSelected) {
756 startIndex = aClickedIndex;
757 endIndex = aClickedIndex;
758 } else if (mStartSelectionIndex <= aClickedIndex) {
759 startIndex = mStartSelectionIndex;
760 endIndex = aClickedIndex;
761 } else {
762 startIndex = aClickedIndex;
763 endIndex = mStartSelectionIndex;
766 // Clear only if control was not pressed
767 wasChanged = ExtendedSelection(startIndex, endIndex, !aIsControl);
768 nsWeakFrame weakFrame(this);
769 ScrollToIndex(aClickedIndex);
770 if (!weakFrame.IsAlive()) {
771 return wasChanged;
774 if (mStartSelectionIndex == kNothingSelected) {
775 mStartSelectionIndex = aClickedIndex;
777 #ifdef ACCESSIBILITY
778 bool isCurrentOptionChanged = mEndSelectionIndex != aClickedIndex;
779 #endif
780 mEndSelectionIndex = aClickedIndex;
781 InvalidateFocus();
783 #ifdef ACCESSIBILITY
784 if (isCurrentOptionChanged) {
785 FireMenuItemActiveEvent();
787 #endif
788 } else if (aIsControl) {
789 wasChanged = SingleSelection(aClickedIndex, true); // might destroy us
790 } else {
791 wasChanged = SingleSelection(aClickedIndex, false); // might destroy us
793 } else {
794 wasChanged = SingleSelection(aClickedIndex, false); // might destroy us
797 return wasChanged;
800 //---------------------------------------------------------
801 bool
802 nsListControlFrame::HandleListSelection(nsIDOMEvent* aEvent,
803 int32_t aClickedIndex)
805 nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
806 bool isShift;
807 bool isControl;
808 #ifdef XP_MACOSX
809 mouseEvent->GetMetaKey(&isControl);
810 #else
811 mouseEvent->GetCtrlKey(&isControl);
812 #endif
813 mouseEvent->GetShiftKey(&isShift);
814 return PerformSelection(aClickedIndex, isShift, isControl); // might destroy us
817 //---------------------------------------------------------
818 void
819 nsListControlFrame::CaptureMouseEvents(bool aGrabMouseEvents)
821 // Currently cocoa widgets use a native popup widget which tracks clicks synchronously,
822 // so we never want to do mouse capturing. Note that we only bail if the list
823 // is in drop-down mode, and the caller is requesting capture (we let release capture
824 // requests go through to ensure that we can release capture requested via other
825 // code paths, if any exist).
826 if (aGrabMouseEvents && IsInDropDownMode() && nsComboboxControlFrame::ToolkitHasNativePopup())
827 return;
829 if (aGrabMouseEvents) {
830 nsIPresShell::SetCapturingContent(mContent, CAPTURE_IGNOREALLOWED);
831 } else {
832 nsIContent* capturingContent = nsIPresShell::GetCapturingContent();
834 bool dropDownIsHidden = false;
835 if (IsInDropDownMode()) {
836 dropDownIsHidden = !mComboboxFrame->IsDroppedDown();
838 if (capturingContent == mContent || dropDownIsHidden) {
839 // only clear the capturing content if *we* are the ones doing the
840 // capturing (or if the dropdown is hidden, in which case NO-ONE should
841 // be capturing anything - it could be a scrollbar inside this listbox
842 // which is actually grabbing
843 // This shouldn't be necessary. We should simply ensure that events targeting
844 // scrollbars are never visible to DOM consumers.
845 nsIPresShell::SetCapturingContent(nullptr, 0);
850 //---------------------------------------------------------
851 NS_IMETHODIMP
852 nsListControlFrame::HandleEvent(nsPresContext* aPresContext,
853 WidgetGUIEvent* aEvent,
854 nsEventStatus* aEventStatus)
856 NS_ENSURE_ARG_POINTER(aEventStatus);
858 /*const char * desc[] = {"NS_MOUSE_MOVE",
859 "NS_MOUSE_LEFT_BUTTON_UP",
860 "NS_MOUSE_LEFT_BUTTON_DOWN",
861 "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
862 "NS_MOUSE_MIDDLE_BUTTON_UP",
863 "NS_MOUSE_MIDDLE_BUTTON_DOWN",
864 "<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>","<NA>",
865 "NS_MOUSE_RIGHT_BUTTON_UP",
866 "NS_MOUSE_RIGHT_BUTTON_DOWN",
867 "NS_MOUSE_ENTER_SYNTH",
868 "NS_MOUSE_EXIT_SYNTH",
869 "NS_MOUSE_LEFT_DOUBLECLICK",
870 "NS_MOUSE_MIDDLE_DOUBLECLICK",
871 "NS_MOUSE_RIGHT_DOUBLECLICK",
872 "NS_MOUSE_LEFT_CLICK",
873 "NS_MOUSE_MIDDLE_CLICK",
874 "NS_MOUSE_RIGHT_CLICK"};
875 int inx = aEvent->message-NS_MOUSE_MESSAGE_START;
876 if (inx >= 0 && inx <= (NS_MOUSE_RIGHT_CLICK-NS_MOUSE_MESSAGE_START)) {
877 printf("Mouse in ListFrame %s [%d]\n", desc[inx], aEvent->message);
878 } else {
879 printf("Mouse in ListFrame <UNKNOWN> [%d]\n", aEvent->message);
882 if (nsEventStatus_eConsumeNoDefault == *aEventStatus)
883 return NS_OK;
885 // do we have style that affects how we are selected?
886 // do we have user-input style?
887 const nsStyleUserInterface* uiStyle = StyleUserInterface();
888 if (uiStyle->mUserInput == NS_STYLE_USER_INPUT_NONE || uiStyle->mUserInput == NS_STYLE_USER_INPUT_DISABLED)
889 return nsFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
891 nsEventStates eventStates = mContent->AsElement()->State();
892 if (eventStates.HasState(NS_EVENT_STATE_DISABLED))
893 return NS_OK;
895 return nsHTMLScrollFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
899 //---------------------------------------------------------
900 NS_IMETHODIMP
901 nsListControlFrame::SetInitialChildList(ChildListID aListID,
902 nsFrameList& aChildList)
904 // First check to see if all the content has been added
905 mIsAllContentHere = mContent->IsDoneAddingChildren();
906 if (!mIsAllContentHere) {
907 mIsAllFramesHere = false;
908 mHasBeenInitialized = false;
910 nsresult rv = nsHTMLScrollFrame::SetInitialChildList(aListID, aChildList);
912 // If all the content is here now check
913 // to see if all the frames have been created
914 /*if (mIsAllContentHere) {
915 // If all content and frames are here
916 // the reset/initialize
917 if (CheckIfAllFramesHere()) {
918 ResetList(aPresContext);
919 mHasBeenInitialized = true;
923 return rv;
926 //---------------------------------------------------------
927 void
928 nsListControlFrame::Init(nsIContent* aContent,
929 nsIFrame* aParent,
930 nsIFrame* aPrevInFlow)
932 nsHTMLScrollFrame::Init(aContent, aParent, aPrevInFlow);
934 // we shouldn't have to unregister this listener because when
935 // our frame goes away all these content node go away as well
936 // because our frame is the only one who references them.
937 // we need to hook up our listeners before the editor is initialized
938 mEventListener = new nsListEventListener(this);
940 mContent->AddSystemEventListener(NS_LITERAL_STRING("keydown"),
941 mEventListener, false, false);
942 mContent->AddSystemEventListener(NS_LITERAL_STRING("keypress"),
943 mEventListener, false, false);
944 mContent->AddSystemEventListener(NS_LITERAL_STRING("mousedown"),
945 mEventListener, false, false);
946 mContent->AddSystemEventListener(NS_LITERAL_STRING("mouseup"),
947 mEventListener, false, false);
948 mContent->AddSystemEventListener(NS_LITERAL_STRING("mousemove"),
949 mEventListener, false, false);
951 mStartSelectionIndex = kNothingSelected;
952 mEndSelectionIndex = kNothingSelected;
954 mLastDropdownBackstopColor = PresContext()->DefaultBackgroundColor();
956 if (IsInDropDownMode()) {
957 AddStateBits(NS_FRAME_IN_POPUP);
961 dom::HTMLOptionsCollection*
962 nsListControlFrame::GetOptions() const
964 dom::HTMLSelectElement* select =
965 dom::HTMLSelectElement::FromContentOrNull(mContent);
966 NS_ENSURE_TRUE(select, nullptr);
968 return select->Options();
971 dom::HTMLOptionElement*
972 nsListControlFrame::GetOption(uint32_t aIndex) const
974 dom::HTMLSelectElement* select =
975 dom::HTMLSelectElement::FromContentOrNull(mContent);
976 NS_ENSURE_TRUE(select, nullptr);
978 return select->Item(aIndex);
981 NS_IMETHODIMP
982 nsListControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected)
984 if (aSelected) {
985 ScrollToIndex(aIndex);
987 return NS_OK;
990 void
991 nsListControlFrame::OnContentReset()
993 ResetList(true);
996 void
997 nsListControlFrame::ResetList(bool aAllowScrolling)
999 // if all the frames aren't here
1000 // don't bother reseting
1001 if (!mIsAllFramesHere) {
1002 return;
1005 if (aAllowScrolling) {
1006 mPostChildrenLoadedReset = true;
1008 // Scroll to the selected index
1009 int32_t indexToSelect = kNothingSelected;
1011 nsCOMPtr<nsIDOMHTMLSelectElement> selectElement(do_QueryInterface(mContent));
1012 NS_ASSERTION(selectElement, "No select element!");
1013 if (selectElement) {
1014 selectElement->GetSelectedIndex(&indexToSelect);
1015 nsWeakFrame weakFrame(this);
1016 ScrollToIndex(indexToSelect);
1017 if (!weakFrame.IsAlive()) {
1018 return;
1023 mStartSelectionIndex = kNothingSelected;
1024 mEndSelectionIndex = kNothingSelected;
1025 InvalidateFocus();
1026 // Combobox will redisplay itself with the OnOptionSelected event
1029 void
1030 nsListControlFrame::SetFocus(bool aOn, bool aRepaint)
1032 InvalidateFocus();
1034 if (aOn) {
1035 ComboboxFocusSet();
1036 mFocused = this;
1037 } else {
1038 mFocused = nullptr;
1041 InvalidateFocus();
1044 void nsListControlFrame::ComboboxFocusSet()
1046 gLastKeyTime = 0;
1049 void
1050 nsListControlFrame::SetComboboxFrame(nsIFrame* aComboboxFrame)
1052 if (nullptr != aComboboxFrame) {
1053 mComboboxFrame = do_QueryFrame(aComboboxFrame);
1057 void
1058 nsListControlFrame::GetOptionText(uint32_t aIndex, nsAString& aStr)
1060 aStr.Truncate();
1061 if (dom::HTMLOptionElement* optionElement = GetOption(aIndex)) {
1062 optionElement->GetText(aStr);
1066 int32_t
1067 nsListControlFrame::GetSelectedIndex()
1069 dom::HTMLSelectElement* select =
1070 dom::HTMLSelectElement::FromContentOrNull(mContent);
1071 return select->SelectedIndex();
1074 dom::HTMLOptionElement*
1075 nsListControlFrame::GetCurrentOption()
1077 // The mEndSelectionIndex is what is currently being selected. Use
1078 // the selected index if this is kNothingSelected.
1079 int32_t focusedIndex = (mEndSelectionIndex == kNothingSelected) ?
1080 GetSelectedIndex() : mEndSelectionIndex;
1082 if (focusedIndex != kNothingSelected) {
1083 return GetOption(SafeCast<uint32_t>(focusedIndex));
1086 // There is no selected item. Return the first non-disabled item.
1087 nsRefPtr<dom::HTMLSelectElement> selectElement =
1088 dom::HTMLSelectElement::FromContent(mContent);
1090 for (uint32_t i = 0, length = selectElement->Length(); i < length; ++i) {
1091 dom::HTMLOptionElement* node = selectElement->Item(i);
1092 if (!node) {
1093 return nullptr;
1096 if (!selectElement->IsOptionDisabled(node)) {
1097 return node;
1101 return nullptr;
1104 bool
1105 nsListControlFrame::IsInDropDownMode() const
1107 return (mComboboxFrame != nullptr);
1110 uint32_t
1111 nsListControlFrame::GetNumberOfOptions()
1113 dom::HTMLOptionsCollection* options = GetOptions();
1114 if (!options) {
1115 return 0;
1118 return options->Length();
1121 //----------------------------------------------------------------------
1122 // nsISelectControlFrame
1123 //----------------------------------------------------------------------
1124 bool nsListControlFrame::CheckIfAllFramesHere()
1126 // Get the number of optgroups and options
1127 //int32_t numContentItems = 0;
1128 nsCOMPtr<nsIDOMNode> node(do_QueryInterface(mContent));
1129 if (node) {
1130 // XXX Need to find a fail proff way to determine that
1131 // all the frames are there
1132 mIsAllFramesHere = true;//NS_OK == CountAllChild(node, numContentItems);
1134 // now make sure we have a frame each piece of content
1136 return mIsAllFramesHere;
1139 NS_IMETHODIMP
1140 nsListControlFrame::DoneAddingChildren(bool aIsDone)
1142 mIsAllContentHere = aIsDone;
1143 if (mIsAllContentHere) {
1144 // Here we check to see if all the frames have been created
1145 // for all the content.
1146 // If so, then we can initialize;
1147 if (!mIsAllFramesHere) {
1148 // if all the frames are now present we can initialize
1149 if (CheckIfAllFramesHere()) {
1150 mHasBeenInitialized = true;
1151 ResetList(true);
1155 return NS_OK;
1158 NS_IMETHODIMP
1159 nsListControlFrame::AddOption(int32_t aIndex)
1161 #ifdef DO_REFLOW_DEBUG
1162 printf("---- Id: %d nsLCF %p Added Option %d\n", mReflowId, this, aIndex);
1163 #endif
1165 if (!mIsAllContentHere) {
1166 mIsAllContentHere = mContent->IsDoneAddingChildren();
1167 if (!mIsAllContentHere) {
1168 mIsAllFramesHere = false;
1169 mHasBeenInitialized = false;
1170 } else {
1171 mIsAllFramesHere = (aIndex == static_cast<int32_t>(GetNumberOfOptions()-1));
1175 // Make sure we scroll to the selected option as needed
1176 mNeedToReset = true;
1178 if (!mHasBeenInitialized) {
1179 return NS_OK;
1182 mPostChildrenLoadedReset = mIsAllContentHere;
1183 return NS_OK;
1186 static int32_t
1187 DecrementAndClamp(int32_t aSelectionIndex, int32_t aLength)
1189 return aLength == 0 ? kNothingSelected : std::max(0, aSelectionIndex - 1);
1192 NS_IMETHODIMP
1193 nsListControlFrame::RemoveOption(int32_t aIndex)
1195 NS_PRECONDITION(aIndex >= 0, "negative <option> index");
1197 // Need to reset if we're a dropdown
1198 if (IsInDropDownMode()) {
1199 mNeedToReset = true;
1200 mPostChildrenLoadedReset = mIsAllContentHere;
1203 if (mStartSelectionIndex != kNothingSelected) {
1204 NS_ASSERTION(mEndSelectionIndex != kNothingSelected, "");
1205 int32_t numOptions = GetNumberOfOptions();
1206 // NOTE: numOptions is the new number of options whereas aIndex is the
1207 // unadjusted index of the removed option (hence the <= below).
1208 NS_ASSERTION(aIndex <= numOptions, "out-of-bounds <option> index");
1210 int32_t forward = mEndSelectionIndex - mStartSelectionIndex;
1211 int32_t* low = forward >= 0 ? &mStartSelectionIndex : &mEndSelectionIndex;
1212 int32_t* high = forward >= 0 ? &mEndSelectionIndex : &mStartSelectionIndex;
1213 if (aIndex < *low)
1214 *low = ::DecrementAndClamp(*low, numOptions);
1215 if (aIndex <= *high)
1216 *high = ::DecrementAndClamp(*high, numOptions);
1217 if (forward == 0)
1218 *low = *high;
1220 else
1221 NS_ASSERTION(mEndSelectionIndex == kNothingSelected, "");
1223 InvalidateFocus();
1224 return NS_OK;
1227 //---------------------------------------------------------
1228 // Set the option selected in the DOM. This method is named
1229 // as it is because it indicates that the frame is the source
1230 // of this event rather than the receiver.
1231 bool
1232 nsListControlFrame::SetOptionsSelectedFromFrame(int32_t aStartIndex,
1233 int32_t aEndIndex,
1234 bool aValue,
1235 bool aClearAll)
1237 nsRefPtr<dom::HTMLSelectElement> selectElement =
1238 dom::HTMLSelectElement::FromContent(mContent);
1240 uint32_t mask = dom::HTMLSelectElement::NOTIFY;
1241 if (aValue) {
1242 mask |= dom::HTMLSelectElement::IS_SELECTED;
1245 if (aClearAll) {
1246 mask |= dom::HTMLSelectElement::CLEAR_ALL;
1249 return selectElement->SetOptionsSelectedByIndex(aStartIndex, aEndIndex, mask);
1252 bool
1253 nsListControlFrame::ToggleOptionSelectedFromFrame(int32_t aIndex)
1255 nsRefPtr<dom::HTMLOptionElement> option =
1256 GetOption(static_cast<uint32_t>(aIndex));
1257 NS_ENSURE_TRUE(option, false);
1259 nsRefPtr<dom::HTMLSelectElement> selectElement =
1260 dom::HTMLSelectElement::FromContent(mContent);
1262 uint32_t mask = dom::HTMLSelectElement::NOTIFY;
1263 if (!option->Selected()) {
1264 mask |= dom::HTMLSelectElement::IS_SELECTED;
1267 return selectElement->SetOptionsSelectedByIndex(aIndex, aIndex, mask);
1271 // Dispatch event and such
1272 bool
1273 nsListControlFrame::UpdateSelection()
1275 if (mIsAllFramesHere) {
1276 // if it's a combobox, display the new text
1277 nsWeakFrame weakFrame(this);
1278 if (mComboboxFrame) {
1279 mComboboxFrame->RedisplaySelectedText();
1281 // if it's a listbox, fire on change
1282 else if (mIsAllContentHere) {
1283 FireOnChange();
1285 return weakFrame.IsAlive();
1287 return true;
1290 void
1291 nsListControlFrame::ComboboxFinish(int32_t aIndex)
1293 gLastKeyTime = 0;
1295 if (mComboboxFrame) {
1296 nsWeakFrame weakFrame(this);
1297 PerformSelection(aIndex, false, false); // might destroy us
1298 if (!weakFrame.IsAlive() || !mComboboxFrame) {
1299 return;
1302 int32_t displayIndex = mComboboxFrame->GetIndexOfDisplayArea();
1303 if (displayIndex != aIndex) {
1304 mComboboxFrame->RedisplaySelectedText(); // might destroy us
1307 if (weakFrame.IsAlive() && mComboboxFrame) {
1308 mComboboxFrame->RollupFromList(); // might destroy us
1313 // Send out an onchange notification.
1314 void
1315 nsListControlFrame::FireOnChange()
1317 if (mComboboxFrame) {
1318 // Return hit without changing anything
1319 int32_t index = mComboboxFrame->UpdateRecentIndex(NS_SKIP_NOTIFY_INDEX);
1320 if (index == NS_SKIP_NOTIFY_INDEX)
1321 return;
1323 // See if the selection actually changed
1324 if (index == GetSelectedIndex())
1325 return;
1328 // Dispatch the change event.
1329 nsContentUtils::DispatchTrustedEvent(mContent->OwnerDoc(), mContent,
1330 NS_LITERAL_STRING("change"), true,
1331 false);
1334 NS_IMETHODIMP
1335 nsListControlFrame::OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex)
1337 if (mComboboxFrame) {
1338 // UpdateRecentIndex with NS_SKIP_NOTIFY_INDEX, so that we won't fire an onchange
1339 // event for this setting of selectedIndex.
1340 mComboboxFrame->UpdateRecentIndex(NS_SKIP_NOTIFY_INDEX);
1343 nsWeakFrame weakFrame(this);
1344 ScrollToIndex(aNewIndex);
1345 if (!weakFrame.IsAlive()) {
1346 return NS_OK;
1348 mStartSelectionIndex = aNewIndex;
1349 mEndSelectionIndex = aNewIndex;
1350 InvalidateFocus();
1352 #ifdef ACCESSIBILITY
1353 FireMenuItemActiveEvent();
1354 #endif
1356 return NS_OK;
1359 //----------------------------------------------------------------------
1360 // End nsISelectControlFrame
1361 //----------------------------------------------------------------------
1363 nsresult
1364 nsListControlFrame::SetFormProperty(nsIAtom* aName,
1365 const nsAString& aValue)
1367 if (nsGkAtoms::selected == aName) {
1368 return NS_ERROR_INVALID_ARG; // Selected is readonly according to spec.
1369 } else if (nsGkAtoms::selectedindex == aName) {
1370 // You shouldn't be calling me for this!!!
1371 return NS_ERROR_INVALID_ARG;
1374 // We should be told about selectedIndex by the DOM element through
1375 // OnOptionSelected
1377 return NS_OK;
1380 void
1381 nsListControlFrame::AboutToDropDown()
1383 NS_ASSERTION(IsInDropDownMode(),
1384 "AboutToDropDown called without being in dropdown mode");
1386 // Our widget doesn't get invalidated on changes to the rest of the document,
1387 // so compute and store this color at the start of a dropdown so we don't
1388 // get weird painting behaviour.
1389 // We start looking for backgrounds above the combobox frame to avoid
1390 // duplicating the combobox frame's background and compose each background
1391 // color we find underneath until we have an opaque color, or run out of
1392 // backgrounds. We compose with the PresContext default background color,
1393 // which is always opaque, in case we don't end up with an opaque color.
1394 // This gives us a very poor approximation of translucency.
1395 nsIFrame* comboboxFrame = do_QueryFrame(mComboboxFrame);
1396 nsStyleContext* context = comboboxFrame->StyleContext()->GetParent();
1397 mLastDropdownBackstopColor = NS_RGBA(0,0,0,0);
1398 while (NS_GET_A(mLastDropdownBackstopColor) < 255 && context) {
1399 mLastDropdownBackstopColor =
1400 NS_ComposeColors(context->StyleBackground()->mBackgroundColor,
1401 mLastDropdownBackstopColor);
1402 context = context->GetParent();
1404 mLastDropdownBackstopColor =
1405 NS_ComposeColors(PresContext()->DefaultBackgroundColor(),
1406 mLastDropdownBackstopColor);
1408 if (mIsAllContentHere && mIsAllFramesHere && mHasBeenInitialized) {
1409 nsWeakFrame weakFrame(this);
1410 ScrollToIndex(GetSelectedIndex());
1411 if (!weakFrame.IsAlive()) {
1412 return;
1414 #ifdef ACCESSIBILITY
1415 FireMenuItemActiveEvent(); // Inform assistive tech what got focus
1416 #endif
1418 mItemSelectionStarted = false;
1421 // We are about to be rolledup from the outside (ComboboxFrame)
1422 void
1423 nsListControlFrame::AboutToRollup()
1425 // We've been updating the combobox with the keyboard up until now, but not
1426 // with the mouse. The problem is, even with mouse selection, we are
1427 // updating the <select>. So if the mouse goes over an option just before
1428 // he leaves the box and clicks, that's what the <select> will show.
1430 // To deal with this we say "whatever is in the combobox is canonical."
1431 // - IF the combobox is different from the current selected index, we
1432 // reset the index.
1434 if (IsInDropDownMode()) {
1435 ComboboxFinish(mComboboxFrame->GetIndexOfDisplayArea()); // might destroy us
1439 NS_IMETHODIMP
1440 nsListControlFrame::DidReflow(nsPresContext* aPresContext,
1441 const nsHTMLReflowState* aReflowState,
1442 nsDidReflowStatus aStatus)
1444 nsresult rv;
1445 bool wasInterrupted = !mHasPendingInterruptAtStartOfReflow &&
1446 aPresContext->HasPendingInterrupt();
1448 rv = nsHTMLScrollFrame::DidReflow(aPresContext, aReflowState, aStatus);
1450 if (mNeedToReset && !wasInterrupted) {
1451 mNeedToReset = false;
1452 // Suppress scrolling to the selected element if we restored
1453 // scroll history state AND the list contents have not changed
1454 // since we loaded all the children AND nothing else forced us
1455 // to scroll by calling ResetList(true). The latter two conditions
1456 // are folded into mPostChildrenLoadedReset.
1458 // The idea is that we want scroll history restoration to trump ResetList
1459 // scrolling to the selected element, when the ResetList was probably only
1460 // caused by content loading normally.
1461 ResetList(!DidHistoryRestore() || mPostChildrenLoadedReset);
1464 mHasPendingInterruptAtStartOfReflow = false;
1465 return rv;
1468 nsIAtom*
1469 nsListControlFrame::GetType() const
1471 return nsGkAtoms::listControlFrame;
1474 #ifdef DEBUG_FRAME_DUMP
1475 NS_IMETHODIMP
1476 nsListControlFrame::GetFrameName(nsAString& aResult) const
1478 return MakeFrameName(NS_LITERAL_STRING("ListControl"), aResult);
1480 #endif
1482 nscoord
1483 nsListControlFrame::GetHeightOfARow()
1485 return HeightOfARow();
1488 nsresult
1489 nsListControlFrame::IsOptionDisabled(int32_t anIndex, bool &aIsDisabled)
1491 nsRefPtr<dom::HTMLSelectElement> sel =
1492 dom::HTMLSelectElement::FromContent(mContent);
1493 if (sel) {
1494 sel->IsOptionDisabled(anIndex, &aIsDisabled);
1495 return NS_OK;
1497 return NS_ERROR_FAILURE;
1500 //----------------------------------------------------------------------
1501 // helper
1502 //----------------------------------------------------------------------
1503 bool
1504 nsListControlFrame::IsLeftButton(nsIDOMEvent* aMouseEvent)
1506 // only allow selection with the left button
1507 nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent);
1508 if (mouseEvent) {
1509 uint16_t whichButton;
1510 if (NS_SUCCEEDED(mouseEvent->GetButton(&whichButton))) {
1511 return whichButton != 0?false:true;
1514 return false;
1517 nscoord
1518 nsListControlFrame::CalcFallbackRowHeight(float aFontSizeInflation)
1520 nscoord rowHeight = 0;
1522 nsRefPtr<nsFontMetrics> fontMet;
1523 nsLayoutUtils::GetFontMetricsForFrame(this, getter_AddRefs(fontMet),
1524 aFontSizeInflation);
1525 if (fontMet) {
1526 rowHeight = fontMet->MaxHeight();
1529 return rowHeight;
1532 nscoord
1533 nsListControlFrame::CalcIntrinsicHeight(nscoord aHeightOfARow,
1534 int32_t aNumberOfOptions)
1536 NS_PRECONDITION(!IsInDropDownMode(),
1537 "Shouldn't be in dropdown mode when we call this");
1539 dom::HTMLSelectElement* select =
1540 dom::HTMLSelectElement::FromContentOrNull(mContent);
1541 if (select) {
1542 mNumDisplayRows = select->Size();
1543 } else {
1544 mNumDisplayRows = 1;
1547 if (mNumDisplayRows < 1) {
1548 mNumDisplayRows = 4;
1551 return mNumDisplayRows * aHeightOfARow;
1554 //----------------------------------------------------------------------
1555 // nsIDOMMouseListener
1556 //----------------------------------------------------------------------
1557 nsresult
1558 nsListControlFrame::MouseUp(nsIDOMEvent* aMouseEvent)
1560 NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
1562 nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent);
1563 NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
1565 UpdateInListState(aMouseEvent);
1567 mButtonDown = false;
1569 nsEventStates eventStates = mContent->AsElement()->State();
1570 if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
1571 return NS_OK;
1574 // only allow selection with the left button
1575 // if a right button click is on the combobox itself
1576 // or on the select when in listbox mode, then let the click through
1577 if (!IsLeftButton(aMouseEvent)) {
1578 if (IsInDropDownMode()) {
1579 if (!IgnoreMouseEventForSelection(aMouseEvent)) {
1580 aMouseEvent->PreventDefault();
1581 aMouseEvent->StopPropagation();
1582 } else {
1583 CaptureMouseEvents(false);
1584 return NS_OK;
1586 CaptureMouseEvents(false);
1587 return NS_ERROR_FAILURE; // means consume event
1588 } else {
1589 CaptureMouseEvents(false);
1590 return NS_OK;
1594 const nsStyleVisibility* vis = StyleVisibility();
1596 if (!vis->IsVisible()) {
1597 return NS_OK;
1600 if (IsInDropDownMode()) {
1601 // XXX This is a bit of a hack, but.....
1602 // But the idea here is to make sure you get an "onclick" event when you mouse
1603 // down on the select and the drag over an option and let go
1604 // And then NOT get an "onclick" event when when you click down on the select
1605 // and then up outside of the select
1606 // the EventStateManager tracks the content of the mouse down and the mouse up
1607 // to make sure they are the same, and the onclick is sent in the PostHandleEvent
1608 // depeneding on whether the clickCount is non-zero.
1609 // So we cheat here by either setting or unsetting the clcikCount in the native event
1610 // so the right thing happens for the onclick event
1611 WidgetMouseEvent* mouseEvent =
1612 aMouseEvent->GetInternalNSEvent()->AsMouseEvent();
1614 int32_t selectedIndex;
1615 if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
1616 // If it's disabled, disallow the click and leave.
1617 bool isDisabled = false;
1618 IsOptionDisabled(selectedIndex, isDisabled);
1619 if (isDisabled) {
1620 aMouseEvent->PreventDefault();
1621 aMouseEvent->StopPropagation();
1622 CaptureMouseEvents(false);
1623 return NS_ERROR_FAILURE;
1626 if (kNothingSelected != selectedIndex) {
1627 nsWeakFrame weakFrame(this);
1628 ComboboxFinish(selectedIndex);
1629 if (!weakFrame.IsAlive())
1630 return NS_OK;
1631 FireOnChange();
1634 mouseEvent->clickCount = 1;
1635 } else {
1636 // the click was out side of the select or its dropdown
1637 mouseEvent->clickCount = IgnoreMouseEventForSelection(aMouseEvent) ? 1 : 0;
1639 } else {
1640 CaptureMouseEvents(false);
1641 // Notify
1642 if (mChangesSinceDragStart) {
1643 // reset this so that future MouseUps without a prior MouseDown
1644 // won't fire onchange
1645 mChangesSinceDragStart = false;
1646 FireOnChange();
1650 return NS_OK;
1653 void
1654 nsListControlFrame::UpdateInListState(nsIDOMEvent* aEvent)
1656 if (!mComboboxFrame || !mComboboxFrame->IsDroppedDown())
1657 return;
1659 nsPoint pt = nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aEvent, this);
1660 nsRect borderInnerEdge = GetScrollPortRect();
1661 if (pt.y >= borderInnerEdge.y && pt.y < borderInnerEdge.YMost()) {
1662 mItemSelectionStarted = true;
1666 bool nsListControlFrame::IgnoreMouseEventForSelection(nsIDOMEvent* aEvent)
1668 if (!mComboboxFrame)
1669 return false;
1671 // Our DOM listener does get called when the dropdown is not
1672 // showing, because it listens to events on the SELECT element
1673 if (!mComboboxFrame->IsDroppedDown())
1674 return true;
1676 return !mItemSelectionStarted;
1679 #ifdef ACCESSIBILITY
1680 void
1681 nsListControlFrame::FireMenuItemActiveEvent()
1683 if (mFocused != this && !IsInDropDownMode()) {
1684 return;
1687 nsCOMPtr<nsIContent> optionContent = GetCurrentOption();
1688 if (!optionContent) {
1689 return;
1692 FireDOMEvent(NS_LITERAL_STRING("DOMMenuItemActive"), optionContent);
1694 #endif
1696 nsresult
1697 nsListControlFrame::GetIndexFromDOMEvent(nsIDOMEvent* aMouseEvent,
1698 int32_t& aCurIndex)
1700 if (IgnoreMouseEventForSelection(aMouseEvent))
1701 return NS_ERROR_FAILURE;
1703 if (nsIPresShell::GetCapturingContent() != mContent) {
1704 // If we're not capturing, then ignore movement in the border
1705 nsPoint pt = nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aMouseEvent, this);
1706 nsRect borderInnerEdge = GetScrollPortRect();
1707 if (!borderInnerEdge.Contains(pt)) {
1708 return NS_ERROR_FAILURE;
1712 nsRefPtr<dom::HTMLOptionElement> option;
1713 for (nsCOMPtr<nsIContent> content =
1714 PresContext()->EventStateManager()->GetEventTargetContent(nullptr);
1715 content && !option;
1716 content = content->GetParent()) {
1717 option = dom::HTMLOptionElement::FromContent(content);
1720 if (option) {
1721 aCurIndex = option->Index();
1722 MOZ_ASSERT(aCurIndex >= 0);
1723 return NS_OK;
1726 int32_t numOptions = GetNumberOfOptions();
1727 if (numOptions < 1)
1728 return NS_ERROR_FAILURE;
1730 nsPoint pt = nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(aMouseEvent, this);
1732 // If the event coordinate is above the first option frame, then target the
1733 // first option frame
1734 nsRefPtr<dom::HTMLOptionElement> firstOption = GetOption(0);
1735 NS_ASSERTION(firstOption, "Can't find first option that's supposed to be there");
1736 nsIFrame* optionFrame = firstOption->GetPrimaryFrame();
1737 if (optionFrame) {
1738 nsPoint ptInOptionFrame = pt - optionFrame->GetOffsetTo(this);
1739 if (ptInOptionFrame.y < 0 && ptInOptionFrame.x >= 0 &&
1740 ptInOptionFrame.x < optionFrame->GetSize().width) {
1741 aCurIndex = 0;
1742 return NS_OK;
1746 nsRefPtr<dom::HTMLOptionElement> lastOption = GetOption(numOptions - 1);
1747 // If the event coordinate is below the last option frame, then target the
1748 // last option frame
1749 NS_ASSERTION(lastOption, "Can't find last option that's supposed to be there");
1750 optionFrame = lastOption->GetPrimaryFrame();
1751 if (optionFrame) {
1752 nsPoint ptInOptionFrame = pt - optionFrame->GetOffsetTo(this);
1753 if (ptInOptionFrame.y >= optionFrame->GetSize().height && ptInOptionFrame.x >= 0 &&
1754 ptInOptionFrame.x < optionFrame->GetSize().width) {
1755 aCurIndex = numOptions - 1;
1756 return NS_OK;
1760 return NS_ERROR_FAILURE;
1763 nsresult
1764 nsListControlFrame::MouseDown(nsIDOMEvent* aMouseEvent)
1766 NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
1768 nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent);
1769 NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
1771 UpdateInListState(aMouseEvent);
1773 nsEventStates eventStates = mContent->AsElement()->State();
1774 if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
1775 return NS_OK;
1778 // only allow selection with the left button
1779 // if a right button click is on the combobox itself
1780 // or on the select when in listbox mode, then let the click through
1781 if (!IsLeftButton(aMouseEvent)) {
1782 if (IsInDropDownMode()) {
1783 if (!IgnoreMouseEventForSelection(aMouseEvent)) {
1784 aMouseEvent->PreventDefault();
1785 aMouseEvent->StopPropagation();
1786 } else {
1787 return NS_OK;
1789 return NS_ERROR_FAILURE; // means consume event
1790 } else {
1791 return NS_OK;
1795 int32_t selectedIndex;
1796 if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
1797 // Handle Like List
1798 mButtonDown = true;
1799 CaptureMouseEvents(true);
1800 nsWeakFrame weakFrame(this);
1801 bool change =
1802 HandleListSelection(aMouseEvent, selectedIndex); // might destroy us
1803 if (!weakFrame.IsAlive()) {
1804 return NS_OK;
1806 mChangesSinceDragStart = change;
1807 } else {
1808 // NOTE: the combo box is responsible for dropping it down
1809 if (mComboboxFrame) {
1810 if (XRE_GetProcessType() == GeckoProcessType_Content && BrowserTabsRemote()) {
1811 nsContentUtils::DispatchChromeEvent(mContent->OwnerDoc(), mContent,
1812 NS_LITERAL_STRING("mozshowdropdown"), true,
1813 false);
1814 return NS_OK;
1817 if (!IgnoreMouseEventForSelection(aMouseEvent)) {
1818 return NS_OK;
1821 if (!nsComboboxControlFrame::ToolkitHasNativePopup())
1823 bool isDroppedDown = mComboboxFrame->IsDroppedDown();
1824 nsIFrame* comboFrame = do_QueryFrame(mComboboxFrame);
1825 nsWeakFrame weakFrame(comboFrame);
1826 mComboboxFrame->ShowDropDown(!isDroppedDown);
1827 if (!weakFrame.IsAlive())
1828 return NS_OK;
1829 if (isDroppedDown) {
1830 CaptureMouseEvents(false);
1836 return NS_OK;
1839 //----------------------------------------------------------------------
1840 // nsIDOMMouseMotionListener
1841 //----------------------------------------------------------------------
1842 nsresult
1843 nsListControlFrame::MouseMove(nsIDOMEvent* aMouseEvent)
1845 NS_ASSERTION(aMouseEvent, "aMouseEvent is null.");
1846 nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent);
1847 NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
1849 UpdateInListState(aMouseEvent);
1851 if (IsInDropDownMode()) {
1852 if (mComboboxFrame->IsDroppedDown()) {
1853 int32_t selectedIndex;
1854 if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
1855 PerformSelection(selectedIndex, false, false); // might destroy us
1858 } else {// XXX - temporary until we get drag events
1859 if (mButtonDown) {
1860 return DragMove(aMouseEvent); // might destroy us
1863 return NS_OK;
1866 nsresult
1867 nsListControlFrame::DragMove(nsIDOMEvent* aMouseEvent)
1869 NS_ASSERTION(aMouseEvent, "aMouseEvent is null.");
1871 UpdateInListState(aMouseEvent);
1873 if (!IsInDropDownMode()) {
1874 int32_t selectedIndex;
1875 if (NS_SUCCEEDED(GetIndexFromDOMEvent(aMouseEvent, selectedIndex))) {
1876 // Don't waste cycles if we already dragged over this item
1877 if (selectedIndex == mEndSelectionIndex) {
1878 return NS_OK;
1880 nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aMouseEvent);
1881 NS_ASSERTION(mouseEvent, "aMouseEvent is not an nsIDOMMouseEvent!");
1882 bool isControl;
1883 #ifdef XP_MACOSX
1884 mouseEvent->GetMetaKey(&isControl);
1885 #else
1886 mouseEvent->GetCtrlKey(&isControl);
1887 #endif
1888 nsWeakFrame weakFrame(this);
1889 // Turn SHIFT on when you are dragging, unless control is on.
1890 bool wasChanged = PerformSelection(selectedIndex,
1891 !isControl, isControl);
1892 if (!weakFrame.IsAlive()) {
1893 return NS_OK;
1895 mChangesSinceDragStart = mChangesSinceDragStart || wasChanged;
1898 return NS_OK;
1901 //----------------------------------------------------------------------
1902 // Scroll helpers.
1903 //----------------------------------------------------------------------
1904 void
1905 nsListControlFrame::ScrollToIndex(int32_t aIndex)
1907 if (aIndex < 0) {
1908 // XXX shouldn't we just do nothing if we're asked to scroll to
1909 // kNothingSelected?
1910 ScrollTo(nsPoint(0, 0), nsIScrollableFrame::INSTANT);
1911 } else {
1912 nsRefPtr<dom::HTMLOptionElement> option =
1913 GetOption(SafeCast<uint32_t>(aIndex));
1914 if (option) {
1915 ScrollToFrame(*option);
1920 void
1921 nsListControlFrame::ScrollToFrame(dom::HTMLOptionElement& aOptElement)
1923 // otherwise we find the content's frame and scroll to it
1924 nsIFrame* childFrame = aOptElement.GetPrimaryFrame();
1925 if (childFrame) {
1926 PresContext()->PresShell()->
1927 ScrollFrameRectIntoView(childFrame,
1928 nsRect(nsPoint(0, 0), childFrame->GetSize()),
1929 nsIPresShell::ScrollAxis(), nsIPresShell::ScrollAxis(),
1930 nsIPresShell::SCROLL_OVERFLOW_HIDDEN |
1931 nsIPresShell::SCROLL_FIRST_ANCESTOR_ONLY);
1935 //---------------------------------------------------------------------
1936 // Ok, the entire idea of this routine is to move to the next item that
1937 // is suppose to be selected. If the item is disabled then we search in
1938 // the same direction looking for the next item to select. If we run off
1939 // the end of the list then we start at the end of the list and search
1940 // backwards until we get back to the original item or an enabled option
1942 // aStartIndex - the index to start searching from
1943 // aNewIndex - will get set to the new index if it finds one
1944 // aNumOptions - the total number of options in the list
1945 // aDoAdjustInc - the initial increment 1-n
1946 // aDoAdjustIncNext - the increment used to search for the next enabled option
1948 // the aDoAdjustInc could be a "1" for a single item or
1949 // any number greater representing a page of items
1951 void
1952 nsListControlFrame::AdjustIndexForDisabledOpt(int32_t aStartIndex,
1953 int32_t &aNewIndex,
1954 int32_t aNumOptions,
1955 int32_t aDoAdjustInc,
1956 int32_t aDoAdjustIncNext)
1958 // Cannot select anything if there is nothing to select
1959 if (aNumOptions == 0) {
1960 aNewIndex = kNothingSelected;
1961 return;
1964 // means we reached the end of the list and now we are searching backwards
1965 bool doingReverse = false;
1966 // lowest index in the search range
1967 int32_t bottom = 0;
1968 // highest index in the search range
1969 int32_t top = aNumOptions;
1971 // Start off keyboard options at selectedIndex if nothing else is defaulted to
1973 // XXX Perhaps this should happen for mouse too, to start off shift click
1974 // automatically in multiple ... to do this, we'd need to override
1975 // OnOptionSelected and set mStartSelectedIndex if nothing is selected. Not
1976 // sure of the effects, though, so I'm not doing it just yet.
1977 int32_t startIndex = aStartIndex;
1978 if (startIndex < bottom) {
1979 startIndex = GetSelectedIndex();
1981 int32_t newIndex = startIndex + aDoAdjustInc;
1983 // make sure we start off in the range
1984 if (newIndex < bottom) {
1985 newIndex = 0;
1986 } else if (newIndex >= top) {
1987 newIndex = aNumOptions-1;
1990 while (1) {
1991 // if the newIndex isn't disabled, we are golden, bail out
1992 bool isDisabled = true;
1993 if (NS_SUCCEEDED(IsOptionDisabled(newIndex, isDisabled)) && !isDisabled) {
1994 break;
1997 // it WAS disabled, so sart looking ahead for the next enabled option
1998 newIndex += aDoAdjustIncNext;
2000 // well, if we reach end reverse the search
2001 if (newIndex < bottom) {
2002 if (doingReverse) {
2003 return; // if we are in reverse mode and reach the end bail out
2004 } else {
2005 // reset the newIndex to the end of the list we hit
2006 // reverse the incrementer
2007 // set the other end of the list to our original starting index
2008 newIndex = bottom;
2009 aDoAdjustIncNext = 1;
2010 doingReverse = true;
2011 top = startIndex;
2013 } else if (newIndex >= top) {
2014 if (doingReverse) {
2015 return; // if we are in reverse mode and reach the end bail out
2016 } else {
2017 // reset the newIndex to the end of the list we hit
2018 // reverse the incrementer
2019 // set the other end of the list to our original starting index
2020 newIndex = top - 1;
2021 aDoAdjustIncNext = -1;
2022 doingReverse = true;
2023 bottom = startIndex;
2028 // Looks like we found one
2029 aNewIndex = newIndex;
2032 nsAString&
2033 nsListControlFrame::GetIncrementalString()
2035 if (sIncrementalString == nullptr)
2036 sIncrementalString = new nsString();
2038 return *sIncrementalString;
2041 void
2042 nsListControlFrame::Shutdown()
2044 delete sIncrementalString;
2045 sIncrementalString = nullptr;
2048 void
2049 nsListControlFrame::DropDownToggleKey(nsIDOMEvent* aKeyEvent)
2051 // Cocoa widgets do native popups, so don't try to show
2052 // dropdowns there.
2053 if (IsInDropDownMode() && !nsComboboxControlFrame::ToolkitHasNativePopup()) {
2054 aKeyEvent->PreventDefault();
2055 if (!mComboboxFrame->IsDroppedDown()) {
2056 mComboboxFrame->ShowDropDown(true);
2057 } else {
2058 nsWeakFrame weakFrame(this);
2059 // mEndSelectionIndex is the last item that got selected.
2060 ComboboxFinish(mEndSelectionIndex);
2061 if (weakFrame.IsAlive()) {
2062 FireOnChange();
2068 nsresult
2069 nsListControlFrame::KeyDown(nsIDOMEvent* aKeyEvent)
2071 MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
2073 nsEventStates eventStates = mContent->AsElement()->State();
2074 if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
2075 return NS_OK;
2078 // Don't check defaultPrevented value because other browsers don't prevent
2079 // the key navigation of list control even if preventDefault() is called.
2081 const WidgetKeyboardEvent* keyEvent =
2082 aKeyEvent->GetInternalNSEvent()->AsKeyboardEvent();
2083 MOZ_ASSERT(keyEvent,
2084 "DOM event must have WidgetKeyboardEvent for its internal event");
2086 if (keyEvent->IsAlt()) {
2087 if (keyEvent->keyCode == NS_VK_UP || keyEvent->keyCode == NS_VK_DOWN) {
2088 DropDownToggleKey(aKeyEvent);
2090 return NS_OK;
2093 // now make sure there are options or we are wasting our time
2094 nsRefPtr<dom::HTMLOptionsCollection> options = GetOptions();
2095 NS_ENSURE_TRUE(options, NS_ERROR_FAILURE);
2097 uint32_t numOptions = options->Length();
2099 // this is the new index to set
2100 int32_t newIndex = kNothingSelected;
2102 bool isControlOrMeta = (keyEvent->IsControl() || keyEvent->IsMeta());
2103 if (isControlOrMeta && (keyEvent->keyCode == NS_VK_UP ||
2104 keyEvent->keyCode == NS_VK_LEFT ||
2105 keyEvent->keyCode == NS_VK_DOWN ||
2106 keyEvent->keyCode == NS_VK_RIGHT)) {
2107 // Don't go into multiple select mode unless this list can handle it
2108 isControlOrMeta = mControlSelectMode = GetMultiple();
2109 } else if (keyEvent->keyCode != NS_VK_SPACE) {
2110 mControlSelectMode = false;
2113 switch (keyEvent->keyCode) {
2114 case NS_VK_UP:
2115 case NS_VK_LEFT:
2116 AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
2117 static_cast<int32_t>(numOptions),
2118 -1, -1);
2119 break;
2120 case NS_VK_DOWN:
2121 case NS_VK_RIGHT:
2122 AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
2123 static_cast<int32_t>(numOptions),
2124 1, 1);
2125 break;
2126 case NS_VK_RETURN:
2127 if (mComboboxFrame) {
2128 nsWeakFrame weakFrame(this);
2129 if (mComboboxFrame->IsDroppedDown()) {
2130 // At closing dropdown, users may not expect there is additional
2131 // behavior for this key event. Therefore, let's consume the event.
2132 aKeyEvent->PreventDefault();
2133 ComboboxFinish(mEndSelectionIndex);
2134 if (!weakFrame.IsAlive()) {
2135 return NS_OK;
2138 FireOnChange();
2139 if (!weakFrame.IsAlive()) {
2140 // If the keydown event causes destroying this, fired keypress on
2141 // another element may cause another action which may not be
2142 // expected by the user.
2143 aKeyEvent->PreventDefault();
2145 return NS_OK;
2147 newIndex = mEndSelectionIndex;
2148 break;
2149 case NS_VK_ESCAPE: {
2150 nsWeakFrame weakFrame(this);
2151 // XXX When the Escape keydown causes closing dropdown, it shouldn't
2152 // cause any additonal actions. We should call preventDefault() here.
2153 AboutToRollup();
2154 if (!weakFrame.IsAlive()) {
2155 // If the keydown event causes destroying this, fired keypress on
2156 // another element may cause another action which may not be
2157 // expected by the user.
2158 aKeyEvent->PreventDefault();
2159 return NS_OK;
2161 break;
2163 case NS_VK_PAGE_UP: {
2164 int32_t itemsPerPage =
2165 std::max(1, static_cast<int32_t>(mNumDisplayRows - 1));
2166 AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
2167 static_cast<int32_t>(numOptions),
2168 -itemsPerPage, -1);
2169 break;
2171 case NS_VK_PAGE_DOWN: {
2172 int32_t itemsPerPage =
2173 std::max(1, static_cast<int32_t>(mNumDisplayRows - 1));
2174 AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
2175 static_cast<int32_t>(numOptions),
2176 itemsPerPage, 1);
2177 break;
2179 case NS_VK_HOME:
2180 AdjustIndexForDisabledOpt(0, newIndex,
2181 static_cast<int32_t>(numOptions),
2182 0, 1);
2183 break;
2184 case NS_VK_END:
2185 AdjustIndexForDisabledOpt(static_cast<int32_t>(numOptions) - 1, newIndex,
2186 static_cast<int32_t>(numOptions),
2187 0, -1);
2188 break;
2190 #if defined(XP_WIN) || defined(XP_OS2)
2191 case NS_VK_F4:
2192 DropDownToggleKey(aKeyEvent);
2193 return NS_OK;
2194 #endif
2196 default: // printable key will be handled by keypress event.
2197 return NS_OK;
2200 aKeyEvent->PreventDefault();
2202 // Cancel incremental search if it's being performed.
2203 GetIncrementalString().Truncate();
2205 // Actually process the new index and let the selection code
2206 // do the scrolling for us
2207 PostHandleKeyEvent(newIndex, 0, keyEvent->IsShift(), isControlOrMeta);
2208 return NS_OK;
2211 nsresult
2212 nsListControlFrame::KeyPress(nsIDOMEvent* aKeyEvent)
2214 MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
2216 nsEventStates eventStates = mContent->AsElement()->State();
2217 if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
2218 return NS_OK;
2221 const WidgetKeyboardEvent* keyEvent =
2222 aKeyEvent->GetInternalNSEvent()->AsKeyboardEvent();
2223 MOZ_ASSERT(keyEvent,
2224 "DOM event must have WidgetKeyboardEvent for its internal event");
2226 // Select option with this as the first character
2227 // XXX Not I18N compliant
2229 // Don't do incremental search if the key event has already consumed.
2230 if (keyEvent->mFlags.mDefaultPrevented) {
2231 return NS_OK;
2234 if (keyEvent->IsAlt()) {
2235 return NS_OK;
2238 // With some keyboard layout, space key causes non-ASCII space.
2239 // So, the check in keydown event handler isn't enough, we need to check it
2240 // again with keypress event.
2241 if (keyEvent->charCode != ' ') {
2242 mControlSelectMode = false;
2245 bool isControlOrMeta = (keyEvent->IsControl() || keyEvent->IsMeta());
2246 if (isControlOrMeta && keyEvent->charCode != ' ') {
2247 return NS_OK;
2250 // NOTE: If keyCode of keypress event is not 0, charCode is always 0.
2251 // Therefore, all non-printable keys are not handled after this block.
2252 if (!keyEvent->charCode) {
2253 // Backspace key will delete the last char in the string
2254 // XXX Backspace key causes "go back the history" on Windows. Shouldn't we
2255 // prevent its default action if incremental search is used since
2256 // getting focus? When I tested this, it worked accidentally.
2257 if (keyEvent->keyCode == NS_VK_BACK && !GetIncrementalString().IsEmpty()) {
2258 GetIncrementalString().Truncate(GetIncrementalString().Length() - 1);
2259 aKeyEvent->PreventDefault();
2261 return NS_OK;
2264 // We ate the key if we got this far.
2265 aKeyEvent->PreventDefault();
2267 // XXX Why don't we check/modify timestamp first?
2269 // Incremental Search: if time elapsed is below
2270 // INCREMENTAL_SEARCH_KEYPRESS_TIME, append this keystroke to the search
2271 // string we will use to find options and start searching at the current
2272 // keystroke. Otherwise, Truncate the string if it's been a long time
2273 // since our last keypress.
2274 if (keyEvent->time - gLastKeyTime > INCREMENTAL_SEARCH_KEYPRESS_TIME) {
2275 // If this is ' ' and we are at the beginning of the string, treat it as
2276 // "select this option" (bug 191543)
2277 if (keyEvent->charCode == ' ') {
2278 // Actually process the new index and let the selection code
2279 // do the scrolling for us
2280 PostHandleKeyEvent(mEndSelectionIndex, keyEvent->charCode,
2281 keyEvent->IsShift(), isControlOrMeta);
2283 return NS_OK;
2286 GetIncrementalString().Truncate();
2289 gLastKeyTime = keyEvent->time;
2291 // Append this keystroke to the search string.
2292 char16_t uniChar = ToLowerCase(static_cast<char16_t>(keyEvent->charCode));
2293 GetIncrementalString().Append(uniChar);
2295 // See bug 188199, if all letters in incremental string are same, just try to
2296 // match the first one
2297 nsAutoString incrementalString(GetIncrementalString());
2298 uint32_t charIndex = 1, stringLength = incrementalString.Length();
2299 while (charIndex < stringLength &&
2300 incrementalString[charIndex] == incrementalString[charIndex - 1]) {
2301 charIndex++;
2303 if (charIndex == stringLength) {
2304 incrementalString.Truncate(1);
2305 stringLength = 1;
2308 // Determine where we're going to start reading the string
2309 // If we have multiple characters to look for, we start looking *at* the
2310 // current option. If we have only one character to look for, we start
2311 // looking *after* the current option.
2312 // Exception: if there is no option selected to start at, we always start
2313 // *at* 0.
2314 int32_t startIndex = GetSelectedIndex();
2315 if (startIndex == kNothingSelected) {
2316 startIndex = 0;
2317 } else if (stringLength == 1) {
2318 startIndex++;
2321 // now make sure there are options or we are wasting our time
2322 nsRefPtr<dom::HTMLOptionsCollection> options = GetOptions();
2323 NS_ENSURE_TRUE(options, NS_ERROR_FAILURE);
2325 uint32_t numOptions = options->Length();
2327 nsWeakFrame weakFrame(this);
2328 for (uint32_t i = 0; i < numOptions; ++i) {
2329 uint32_t index = (i + startIndex) % numOptions;
2330 nsRefPtr<dom::HTMLOptionElement> optionElement =
2331 options->ItemAsOption(index);
2332 if (!optionElement || !optionElement->GetPrimaryFrame()) {
2333 continue;
2336 nsAutoString text;
2337 if (NS_FAILED(optionElement->GetText(text)) ||
2338 !StringBeginsWith(
2339 nsContentUtils::TrimWhitespace<
2340 nsContentUtils::IsHTMLWhitespaceOrNBSP>(text, false),
2341 incrementalString, nsCaseInsensitiveStringComparator())) {
2342 continue;
2345 bool wasChanged = PerformSelection(index, keyEvent->IsShift(), isControlOrMeta);
2346 if (!weakFrame.IsAlive()) {
2347 return NS_OK;
2349 if (!wasChanged) {
2350 break;
2353 // If UpdateSelection() returns false, that means the frame is no longer
2354 // alive. We should stop doing anything.
2355 if (!UpdateSelection()) {
2356 return NS_OK;
2358 break;
2361 return NS_OK;
2364 void
2365 nsListControlFrame::PostHandleKeyEvent(int32_t aNewIndex,
2366 uint32_t aCharCode,
2367 bool aIsShift,
2368 bool aIsControlOrMeta)
2370 if (aNewIndex == kNothingSelected) {
2371 return;
2374 // If you hold control, but not shift, no key will actually do anything
2375 // except space.
2376 nsWeakFrame weakFrame(this);
2377 bool wasChanged = false;
2378 if (aIsControlOrMeta && !aIsShift && aCharCode != ' ') {
2379 mStartSelectionIndex = aNewIndex;
2380 mEndSelectionIndex = aNewIndex;
2381 InvalidateFocus();
2382 ScrollToIndex(aNewIndex);
2383 if (!weakFrame.IsAlive()) {
2384 return;
2387 #ifdef ACCESSIBILITY
2388 FireMenuItemActiveEvent();
2389 #endif
2390 } else if (mControlSelectMode && aCharCode == ' ') {
2391 wasChanged = SingleSelection(aNewIndex, true);
2392 } else {
2393 wasChanged = PerformSelection(aNewIndex, aIsShift, aIsControlOrMeta);
2395 if (wasChanged && weakFrame.IsAlive()) {
2396 // dispatch event, update combobox, etc.
2397 UpdateSelection();
2402 /******************************************************************************
2403 * nsListEventListener
2404 *****************************************************************************/
2406 NS_IMPL_ISUPPORTS1(nsListEventListener, nsIDOMEventListener)
2408 NS_IMETHODIMP
2409 nsListEventListener::HandleEvent(nsIDOMEvent* aEvent)
2411 if (!mFrame)
2412 return NS_OK;
2414 nsAutoString eventType;
2415 aEvent->GetType(eventType);
2416 if (eventType.EqualsLiteral("keydown"))
2417 return mFrame->nsListControlFrame::KeyDown(aEvent);
2418 if (eventType.EqualsLiteral("keypress"))
2419 return mFrame->nsListControlFrame::KeyPress(aEvent);
2420 if (eventType.EqualsLiteral("mousedown"))
2421 return mFrame->nsListControlFrame::MouseDown(aEvent);
2422 if (eventType.EqualsLiteral("mouseup"))
2423 return mFrame->nsListControlFrame::MouseUp(aEvent);
2424 if (eventType.EqualsLiteral("mousemove"))
2425 return mFrame->nsListControlFrame::MouseMove(aEvent);
2427 NS_ABORT();
2428 return NS_OK;