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