Bug 1867190 - Initialise the PHC allocate delay later r=glandium
[gecko.git] / layout / forms / HTMLSelectEventListener.cpp
blob4f6b1e356177f63ca22dc69aea8348bc94b0d5b5
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "HTMLSelectEventListener.h"
8 #include "nsListControlFrame.h"
9 #include "mozilla/dom/Event.h"
10 #include "mozilla/dom/MouseEvent.h"
11 #include "mozilla/Casting.h"
12 #include "mozilla/MouseEvents.h"
13 #include "mozilla/TextEvents.h"
14 #include "mozilla/PresShell.h"
15 #include "mozilla/dom/HTMLSelectElement.h"
16 #include "mozilla/dom/HTMLOptionElement.h"
17 #include "mozilla/StaticPrefs_ui.h"
18 #include "mozilla/ClearOnShutdown.h"
20 using namespace mozilla;
21 using namespace mozilla::dom;
23 static bool IsOptionInteractivelySelectable(HTMLSelectElement& aSelect,
24 HTMLOptionElement& aOption,
25 bool aIsCombobox) {
26 if (aSelect.IsOptionDisabled(&aOption)) {
27 return false;
29 if (!aIsCombobox) {
30 return aOption.GetPrimaryFrame();
32 // In dropdown mode no options have frames, but we can check whether they
33 // are rendered / not in a display: none subtree.
34 if (!aOption.HasServoData() || Servo_Element_IsDisplayNone(&aOption)) {
35 return false;
37 // TODO(emilio): This is a bit silly and doesn't match the options that we
38 // show / don't show in the dropdown, but matches the frame construction we
39 // do for multiple selects. For backwards compat also don't allow selecting
40 // options in a display: contents subtree interactively.
41 // test_select_key_navigation_bug1498769.html tests for this and should
42 // probably be changed (and this loop removed) or alternatively
43 // SelectChild.jsm should be changed to match it.
44 for (Element* el = &aOption; el && el != &aSelect;
45 el = el->GetParentElement()) {
46 if (Servo_Element_IsDisplayContents(el)) {
47 return false;
50 return true;
53 namespace mozilla {
55 static StaticAutoPtr<nsString> sIncrementalString;
56 static TimeStamp gLastKeyTime;
57 static uintptr_t sLastKeyListener = 0;
58 static constexpr int32_t kNothingSelected = -1;
60 static nsString& GetIncrementalString() {
61 MOZ_ASSERT(sLastKeyListener != 0);
62 if (!sIncrementalString) {
63 sIncrementalString = new nsString();
64 ClearOnShutdown(&sIncrementalString);
66 return *sIncrementalString;
69 class MOZ_RAII AutoIncrementalSearchHandler {
70 public:
71 explicit AutoIncrementalSearchHandler(HTMLSelectEventListener& aListener) {
72 if (sLastKeyListener != uintptr_t(&aListener)) {
73 sLastKeyListener = uintptr_t(&aListener);
74 GetIncrementalString().Truncate();
75 // To make it easier to handle time comparisons in the other methods,
76 // initialize gLastKeyTime to a value in the past.
77 gLastKeyTime = TimeStamp::Now() -
78 TimeDuration::FromMilliseconds(
79 StaticPrefs::ui_menu_incremental_search_timeout() * 2);
82 ~AutoIncrementalSearchHandler() {
83 if (!mResettingCancelled) {
84 GetIncrementalString().Truncate();
87 void CancelResetting() { mResettingCancelled = true; }
89 private:
90 bool mResettingCancelled = false;
93 NS_IMPL_ISUPPORTS(HTMLSelectEventListener, nsIMutationObserver,
94 nsIDOMEventListener)
96 HTMLSelectEventListener::~HTMLSelectEventListener() {
97 if (sLastKeyListener == uintptr_t(this)) {
98 sLastKeyListener = 0;
102 nsListControlFrame* HTMLSelectEventListener::GetListControlFrame() const {
103 if (mIsCombobox) {
104 MOZ_ASSERT(!mElement->GetPrimaryFrame() ||
105 !mElement->GetPrimaryFrame()->IsListControlFrame());
106 return nullptr;
108 return do_QueryFrame(mElement->GetPrimaryFrame());
111 int32_t HTMLSelectEventListener::GetEndSelectionIndex() const {
112 if (auto* lf = GetListControlFrame()) {
113 return lf->GetEndSelectionIndex();
115 // Combobox selects only have one selected index, so the end and start is the
116 // same.
117 return mElement->SelectedIndex();
120 bool HTMLSelectEventListener::IsOptionInteractivelySelectable(
121 uint32_t aIndex) const {
122 HTMLOptionElement* option = mElement->Item(aIndex);
123 return option &&
124 ::IsOptionInteractivelySelectable(*mElement, *option, mIsCombobox);
127 //---------------------------------------------------------------------
128 // Ok, the entire idea of this routine is to move to the next item that
129 // is suppose to be selected. If the item is disabled then we search in
130 // the same direction looking for the next item to select. If we run off
131 // the end of the list then we start at the end of the list and search
132 // backwards until we get back to the original item or an enabled option
134 // aStartIndex - the index to start searching from
135 // aNewIndex - will get set to the new index if it finds one
136 // aNumOptions - the total number of options in the list
137 // aDoAdjustInc - the initial index increment / decrement
138 // aDoAdjustIncNext - the subsequent index increment/decrement used to search
139 // for the next enabled option
141 // the aDoAdjustInc could be a "1" for a single item or
142 // any number greater representing a page of items
144 void HTMLSelectEventListener::AdjustIndexForDisabledOpt(
145 int32_t aStartIndex, int32_t& aNewIndex, int32_t aNumOptions,
146 int32_t aDoAdjustInc, int32_t aDoAdjustIncNext) {
147 // Cannot select anything if there is nothing to select
148 if (aNumOptions == 0) {
149 aNewIndex = kNothingSelected;
150 return;
153 // means we reached the end of the list and now we are searching backwards
154 bool doingReverse = false;
155 // lowest index in the search range
156 int32_t bottom = 0;
157 // highest index in the search range
158 int32_t top = aNumOptions;
160 // Start off keyboard options at selectedIndex if nothing else is defaulted to
162 // XXX Perhaps this should happen for mouse too, to start off shift click
163 // automatically in multiple ... to do this, we'd need to override
164 // OnOptionSelected and set mStartSelectedIndex if nothing is selected. Not
165 // sure of the effects, though, so I'm not doing it just yet.
166 int32_t startIndex = aStartIndex;
167 if (startIndex < bottom) {
168 startIndex = mElement->SelectedIndex();
170 int32_t newIndex = startIndex + aDoAdjustInc;
172 // make sure we start off in the range
173 if (newIndex < bottom) {
174 newIndex = 0;
175 } else if (newIndex >= top) {
176 newIndex = aNumOptions - 1;
179 while (true) {
180 // if the newIndex is selectable, we are golden, bail out
181 if (IsOptionInteractivelySelectable(newIndex)) {
182 break;
185 // it WAS disabled, so sart looking ahead for the next enabled option
186 newIndex += aDoAdjustIncNext;
188 // well, if we reach end reverse the search
189 if (newIndex < bottom) {
190 if (doingReverse) {
191 return; // if we are in reverse mode and reach the end bail out
193 // reset the newIndex to the end of the list we hit
194 // reverse the incrementer
195 // set the other end of the list to our original starting index
196 newIndex = bottom;
197 aDoAdjustIncNext = 1;
198 doingReverse = true;
199 top = startIndex;
200 } else if (newIndex >= top) {
201 if (doingReverse) {
202 return; // if we are in reverse mode and reach the end bail out
204 // reset the newIndex to the end of the list we hit
205 // reverse the incrementer
206 // set the other end of the list to our original starting index
207 newIndex = top - 1;
208 aDoAdjustIncNext = -1;
209 doingReverse = true;
210 bottom = startIndex;
214 // Looks like we found one
215 aNewIndex = newIndex;
218 NS_IMETHODIMP
219 HTMLSelectEventListener::HandleEvent(dom::Event* aEvent) {
220 nsAutoString eventType;
221 aEvent->GetType(eventType);
222 if (eventType.EqualsLiteral("keydown")) {
223 return KeyDown(aEvent);
225 if (eventType.EqualsLiteral("keypress")) {
226 return KeyPress(aEvent);
228 if (eventType.EqualsLiteral("mousedown")) {
229 if (aEvent->DefaultPrevented()) {
230 return NS_OK;
232 return MouseDown(aEvent);
234 if (eventType.EqualsLiteral("mouseup")) {
235 // Don't try to honor defaultPrevented here - it's not web compatible.
236 // (bug 1194733)
237 return MouseUp(aEvent);
239 if (eventType.EqualsLiteral("mousemove")) {
240 // I don't think we want to honor defaultPrevented on mousemove
241 // in general, and it would only prevent highlighting here.
242 return MouseMove(aEvent);
245 MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
246 return NS_OK;
249 void HTMLSelectEventListener::Attach() {
250 mElement->AddSystemEventListener(u"keydown"_ns, this, false, false);
251 mElement->AddSystemEventListener(u"keypress"_ns, this, false, false);
252 mElement->AddSystemEventListener(u"mousedown"_ns, this, false, false);
253 mElement->AddSystemEventListener(u"mouseup"_ns, this, false, false);
254 mElement->AddSystemEventListener(u"mousemove"_ns, this, false, false);
256 if (mIsCombobox) {
257 mElement->AddMutationObserver(this);
261 void HTMLSelectEventListener::Detach() {
262 mElement->RemoveSystemEventListener(u"keydown"_ns, this, false);
263 mElement->RemoveSystemEventListener(u"keypress"_ns, this, false);
264 mElement->RemoveSystemEventListener(u"mousedown"_ns, this, false);
265 mElement->RemoveSystemEventListener(u"mouseup"_ns, this, false);
266 mElement->RemoveSystemEventListener(u"mousemove"_ns, this, false);
268 if (mIsCombobox) {
269 mElement->RemoveMutationObserver(this);
270 if (mElement->OpenInParentProcess()) {
271 nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
272 "HTMLSelectEventListener::Detach", [element = mElement] {
273 // Don't hide the dropdown if the element has another frame already,
274 // this prevents closing dropdowns on reframe, see bug 1440506.
276 // FIXME(emilio): The flush is needed to deal with reframes started
277 // from DOM node removal. But perhaps we can be a bit smarter here.
278 if (!element->IsCombobox() ||
279 !element->GetPrimaryFrame(FlushType::Frames)) {
280 nsContentUtils::DispatchChromeEvent(
281 element->OwnerDoc(), element, u"mozhidedropdown"_ns,
282 CanBubble::eYes, Cancelable::eNo);
284 }));
289 const uint32_t kMaxDropdownRows = 20; // matches the setting for 4.x browsers
291 int32_t HTMLSelectEventListener::ItemsPerPage() const {
292 uint32_t size = [&] {
293 if (mIsCombobox) {
294 return kMaxDropdownRows;
296 if (auto* lf = GetListControlFrame()) {
297 return lf->GetNumDisplayRows();
299 return mElement->Size();
300 }();
301 if (size <= 1) {
302 return 1;
304 if (MOZ_UNLIKELY(size > INT32_MAX)) {
305 return INT32_MAX - 1;
307 return AssertedCast<int32_t>(size - 1u);
310 void HTMLSelectEventListener::OptionValueMightHaveChanged(
311 nsIContent* aMutatingNode) {
312 #ifdef ACCESSIBILITY
313 if (nsAccessibilityService* acc = GetAccService()) {
314 acc->ComboboxOptionMaybeChanged(mElement->OwnerDoc()->GetPresShell(),
315 aMutatingNode);
317 #endif
320 void HTMLSelectEventListener::AttributeChanged(dom::Element* aElement,
321 int32_t aNameSpaceID,
322 nsAtom* aAttribute,
323 int32_t aModType,
324 const nsAttrValue* aOldValue) {
325 if (aElement->IsHTMLElement(nsGkAtoms::option) &&
326 aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::label) {
327 // A11y has its own mutation listener for this so no need to do
328 // OptionValueMightHaveChanged().
329 ComboboxMightHaveChanged();
333 void HTMLSelectEventListener::CharacterDataChanged(
334 nsIContent* aContent, const CharacterDataChangeInfo&) {
335 if (nsContentUtils::IsInSameAnonymousTree(mElement, aContent)) {
336 OptionValueMightHaveChanged(aContent);
337 ComboboxMightHaveChanged();
341 void HTMLSelectEventListener::ContentRemoved(nsIContent* aChild,
342 nsIContent* aPreviousSibling) {
343 if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) {
344 OptionValueMightHaveChanged(aChild);
345 ComboboxMightHaveChanged();
349 void HTMLSelectEventListener::ContentAppended(nsIContent* aFirstNewContent) {
350 if (nsContentUtils::IsInSameAnonymousTree(mElement, aFirstNewContent)) {
351 OptionValueMightHaveChanged(aFirstNewContent);
352 ComboboxMightHaveChanged();
356 void HTMLSelectEventListener::ContentInserted(nsIContent* aChild) {
357 if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) {
358 OptionValueMightHaveChanged(aChild);
359 ComboboxMightHaveChanged();
363 void HTMLSelectEventListener::ComboboxMightHaveChanged() {
364 if (nsIFrame* f = mElement->GetPrimaryFrame()) {
365 PresShell* ps = f->PresShell();
366 // nsComoboxControlFrame::Reflow updates the selected text. AddOption /
367 // RemoveOption / etc takes care of keeping the displayed index up to date.
368 ps->FrameNeedsReflow(f, IntrinsicDirty::FrameAncestorsAndDescendants,
369 NS_FRAME_IS_DIRTY);
370 #ifdef ACCESSIBILITY
371 if (nsAccessibilityService* acc = GetAccService()) {
372 acc->ScheduleAccessibilitySubtreeUpdate(ps, mElement);
374 #endif
378 void HTMLSelectEventListener::FireOnInputAndOnChange() {
379 RefPtr<HTMLSelectElement> element = mElement;
380 element->UserFinishedInteracting(/* aChanged = */ true);
383 static void FireDropDownEvent(HTMLSelectElement* aElement, bool aShow,
384 bool aIsSourceTouchEvent) {
385 const auto eventName = [&] {
386 if (aShow) {
387 return aIsSourceTouchEvent ? u"mozshowdropdown-sourcetouch"_ns
388 : u"mozshowdropdown"_ns;
390 return u"mozhidedropdown"_ns;
391 }();
392 nsContentUtils::DispatchChromeEvent(aElement->OwnerDoc(), aElement, eventName,
393 CanBubble::eYes, Cancelable::eNo);
396 nsresult HTMLSelectEventListener::MouseDown(dom::Event* aMouseEvent) {
397 NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
399 MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
400 NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
402 if (mElement->State().HasState(ElementState::DISABLED)) {
403 return NS_OK;
406 // only allow selection with the left button
407 // if a right button click is on the combobox itself
408 // or on the select when in listbox mode, then let the click through
409 const bool isLeftButton = mouseEvent->Button() == 0;
410 if (!isLeftButton) {
411 return NS_OK;
414 if (mIsCombobox) {
415 uint16_t inputSource = mouseEvent->InputSource();
416 if (mElement->OpenInParentProcess()) {
417 nsCOMPtr<nsIContent> target = do_QueryInterface(aMouseEvent->GetTarget());
418 if (target && target->IsHTMLElement(nsGkAtoms::option)) {
419 return NS_OK;
423 const bool isSourceTouchEvent =
424 inputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH;
425 FireDropDownEvent(mElement, !mElement->OpenInParentProcess(),
426 isSourceTouchEvent);
427 return NS_OK;
430 if (nsListControlFrame* list = GetListControlFrame()) {
431 mButtonDown = true;
432 return list->HandleLeftButtonMouseDown(aMouseEvent);
434 return NS_OK;
437 nsresult HTMLSelectEventListener::MouseUp(dom::Event* aMouseEvent) {
438 NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
440 MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
441 NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
443 mButtonDown = false;
445 if (mElement->State().HasState(ElementState::DISABLED)) {
446 return NS_OK;
449 if (nsListControlFrame* lf = GetListControlFrame()) {
450 lf->CaptureMouseEvents(false);
453 // only allow selection with the left button
454 // if a right button click is on the combobox itself
455 // or on the select when in listbox mode, then let the click through
456 const bool isLeftButton = mouseEvent->Button() == 0;
457 if (!isLeftButton) {
458 return NS_OK;
461 if (nsListControlFrame* lf = GetListControlFrame()) {
462 return lf->HandleLeftButtonMouseUp(aMouseEvent);
465 return NS_OK;
468 nsresult HTMLSelectEventListener::MouseMove(dom::Event* aMouseEvent) {
469 NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
471 MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
472 NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
474 if (!mButtonDown) {
475 return NS_OK;
478 if (nsListControlFrame* lf = GetListControlFrame()) {
479 return lf->DragMove(aMouseEvent);
482 return NS_OK;
485 nsresult HTMLSelectEventListener::KeyPress(dom::Event* aKeyEvent) {
486 MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
488 if (mElement->State().HasState(ElementState::DISABLED)) {
489 return NS_OK;
492 AutoIncrementalSearchHandler incrementalHandler(*this);
494 const WidgetKeyboardEvent* keyEvent =
495 aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
496 MOZ_ASSERT(keyEvent,
497 "DOM event must have WidgetKeyboardEvent for its internal event");
499 // Select option with this as the first character
500 // XXX Not I18N compliant
502 // Don't do incremental search if the key event has already consumed.
503 if (keyEvent->DefaultPrevented()) {
504 return NS_OK;
507 if (keyEvent->IsAlt()) {
508 return NS_OK;
511 // With some keyboard layout, space key causes non-ASCII space.
512 // So, the check in keydown event handler isn't enough, we need to check it
513 // again with keypress event.
514 if (keyEvent->mCharCode != ' ') {
515 mControlSelectMode = false;
518 const bool isControlOrMeta =
519 keyEvent->IsControl()
520 #if !defined(XP_WIN) && !defined(MOZ_WIDGET_GTK)
521 // Ignore Windows Logo key press in Win/Linux because it's not a usual
522 // modifier for applications. Here wants to check "Accel" like modifier.
523 || keyEvent->IsMeta()
524 #endif
526 if (isControlOrMeta && keyEvent->mCharCode != ' ') {
527 return NS_OK;
530 // NOTE: If mKeyCode of keypress event is not 0, mCharCode is always 0.
531 // Therefore, all non-printable keys are not handled after this block.
532 if (!keyEvent->mCharCode) {
533 // Backspace key will delete the last char in the string. Otherwise,
534 // non-printable keypress should reset incremental search.
535 if (keyEvent->mKeyCode == NS_VK_BACK) {
536 incrementalHandler.CancelResetting();
537 if (!GetIncrementalString().IsEmpty()) {
538 GetIncrementalString().Truncate(GetIncrementalString().Length() - 1);
540 aKeyEvent->PreventDefault();
541 } else {
542 // XXX When a select element has focus, even if the key causes nothing,
543 // it might be better to call preventDefault() here because nobody
544 // should expect one of other elements including chrome handles the
545 // key event.
547 return NS_OK;
550 incrementalHandler.CancelResetting();
552 // We ate the key if we got this far.
553 aKeyEvent->PreventDefault();
555 // XXX Why don't we check/modify timestamp first?
557 // Incremental Search: if time elapsed is below
558 // ui.menu.incremental_search.timeout, append this keystroke to the search
559 // string we will use to find options and start searching at the current
560 // keystroke. Otherwise, Truncate the string if it's been a long time
561 // since our last keypress.
562 if ((keyEvent->mTimeStamp - gLastKeyTime).ToMilliseconds() >
563 StaticPrefs::ui_menu_incremental_search_timeout()) {
564 // If this is ' ' and we are at the beginning of the string, treat it as
565 // "select this option" (bug 191543)
566 if (keyEvent->mCharCode == ' ') {
567 // Actually process the new index and let the selection code
568 // do the scrolling for us
569 PostHandleKeyEvent(GetEndSelectionIndex(), keyEvent->mCharCode,
570 keyEvent->IsShift(), isControlOrMeta);
572 return NS_OK;
575 GetIncrementalString().Truncate();
578 gLastKeyTime = keyEvent->mTimeStamp;
580 // Append this keystroke to the search string.
581 char16_t uniChar = ToLowerCase(static_cast<char16_t>(keyEvent->mCharCode));
582 GetIncrementalString().Append(uniChar);
584 // See bug 188199, if all letters in incremental string are same, just try to
585 // match the first one
586 nsAutoString incrementalString(GetIncrementalString());
587 uint32_t charIndex = 1, stringLength = incrementalString.Length();
588 while (charIndex < stringLength &&
589 incrementalString[charIndex] == incrementalString[charIndex - 1]) {
590 charIndex++;
592 if (charIndex == stringLength) {
593 incrementalString.Truncate(1);
594 stringLength = 1;
597 // Determine where we're going to start reading the string
598 // If we have multiple characters to look for, we start looking *at* the
599 // current option. If we have only one character to look for, we start
600 // looking *after* the current option.
601 // Exception: if there is no option selected to start at, we always start
602 // *at* 0.
603 int32_t startIndex = mElement->SelectedIndex();
604 if (startIndex == kNothingSelected) {
605 startIndex = 0;
606 } else if (stringLength == 1) {
607 startIndex++;
610 // now make sure there are options or we are wasting our time
611 RefPtr<dom::HTMLOptionsCollection> options = mElement->Options();
612 uint32_t numOptions = options->Length();
614 for (uint32_t i = 0; i < numOptions; ++i) {
615 uint32_t index = (i + startIndex) % numOptions;
616 RefPtr<dom::HTMLOptionElement> optionElement = options->ItemAsOption(index);
617 if (!optionElement || !::IsOptionInteractivelySelectable(
618 *mElement, *optionElement, mIsCombobox)) {
619 continue;
622 nsAutoString text;
623 optionElement->GetRenderedLabel(text);
624 if (!StringBeginsWith(
625 nsContentUtils::TrimWhitespace<
626 nsContentUtils::IsHTMLWhitespaceOrNBSP>(text, false),
627 incrementalString, nsCaseInsensitiveStringComparator)) {
628 continue;
631 if (mIsCombobox) {
632 if (optionElement->Selected()) {
633 return NS_OK;
635 optionElement->SetSelected(true);
636 FireOnInputAndOnChange();
637 return NS_OK;
640 if (nsListControlFrame* lf = GetListControlFrame()) {
641 bool wasChanged =
642 lf->PerformSelection(index, keyEvent->IsShift(), isControlOrMeta);
643 if (!wasChanged) {
644 return NS_OK;
646 FireOnInputAndOnChange();
648 break;
651 return NS_OK;
654 nsresult HTMLSelectEventListener::KeyDown(dom::Event* aKeyEvent) {
655 MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
657 if (mElement->State().HasState(ElementState::DISABLED)) {
658 return NS_OK;
661 AutoIncrementalSearchHandler incrementalHandler(*this);
663 if (aKeyEvent->DefaultPrevented()) {
664 return NS_OK;
667 const WidgetKeyboardEvent* keyEvent =
668 aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
669 MOZ_ASSERT(keyEvent,
670 "DOM event must have WidgetKeyboardEvent for its internal event");
672 bool dropDownMenuOnUpDown;
673 bool dropDownMenuOnSpace;
674 #ifdef XP_MACOSX
675 dropDownMenuOnUpDown = mIsCombobox && !mElement->OpenInParentProcess();
676 dropDownMenuOnSpace = mIsCombobox && !keyEvent->IsAlt() &&
677 !keyEvent->IsControl() && !keyEvent->IsMeta();
678 #else
679 dropDownMenuOnUpDown = mIsCombobox && keyEvent->IsAlt();
680 dropDownMenuOnSpace = mIsCombobox && !mElement->OpenInParentProcess();
681 #endif
682 bool withinIncrementalSearchTime =
683 (keyEvent->mTimeStamp - gLastKeyTime).ToMilliseconds() <=
684 StaticPrefs::ui_menu_incremental_search_timeout();
685 if ((dropDownMenuOnUpDown &&
686 (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_DOWN)) ||
687 (dropDownMenuOnSpace && keyEvent->mKeyCode == NS_VK_SPACE &&
688 !withinIncrementalSearchTime)) {
689 FireDropDownEvent(mElement, !mElement->OpenInParentProcess(), false);
690 aKeyEvent->PreventDefault();
691 return NS_OK;
693 if (keyEvent->IsAlt()) {
694 return NS_OK;
697 // We should not change the selection if the popup is "opened in the parent
698 // process" (even when we're in single-process mode).
699 const bool shouldSelect = !mIsCombobox || !mElement->OpenInParentProcess();
701 // now make sure there are options or we are wasting our time
702 RefPtr<dom::HTMLOptionsCollection> options = mElement->Options();
703 uint32_t numOptions = options->Length();
705 // this is the new index to set
706 int32_t newIndex = kNothingSelected;
708 bool isControlOrMeta =
709 keyEvent->IsControl()
710 #if !defined(XP_WIN) && !defined(MOZ_WIDGET_GTK)
711 // Ignore Windows Logo key press in Win/Linux because it's not a usual
712 // modifier for applications. Here wants to check "Accel" like modifier.
713 || keyEvent->IsMeta()
714 #endif
716 // Don't try to handle multiple-select pgUp/pgDown in single-select lists.
717 if (isControlOrMeta && !mElement->Multiple() &&
718 (keyEvent->mKeyCode == NS_VK_PAGE_UP ||
719 keyEvent->mKeyCode == NS_VK_PAGE_DOWN)) {
720 return NS_OK;
722 if (isControlOrMeta &&
723 (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_LEFT ||
724 keyEvent->mKeyCode == NS_VK_DOWN || keyEvent->mKeyCode == NS_VK_RIGHT ||
725 keyEvent->mKeyCode == NS_VK_HOME || keyEvent->mKeyCode == NS_VK_END)) {
726 // Don't go into multiple-select mode unless this list can handle it.
727 isControlOrMeta = mControlSelectMode = mElement->Multiple();
728 } else if (keyEvent->mKeyCode != NS_VK_SPACE) {
729 mControlSelectMode = false;
732 switch (keyEvent->mKeyCode) {
733 case NS_VK_UP:
734 case NS_VK_LEFT:
735 if (shouldSelect) {
736 AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex,
737 int32_t(numOptions), -1, -1);
739 break;
740 case NS_VK_DOWN:
741 case NS_VK_RIGHT:
742 if (shouldSelect) {
743 AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex,
744 int32_t(numOptions), 1, 1);
746 break;
747 case NS_VK_RETURN:
748 // If this is single select listbox, Enter key doesn't cause anything.
749 if (!mElement->Multiple()) {
750 return NS_OK;
753 newIndex = GetEndSelectionIndex();
754 break;
755 case NS_VK_PAGE_UP: {
756 if (shouldSelect) {
757 AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex,
758 int32_t(numOptions), -ItemsPerPage(), -1);
760 break;
762 case NS_VK_PAGE_DOWN: {
763 if (shouldSelect) {
764 AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex,
765 int32_t(numOptions), ItemsPerPage(), 1);
767 break;
769 case NS_VK_HOME:
770 if (shouldSelect) {
771 AdjustIndexForDisabledOpt(0, newIndex, int32_t(numOptions), 0, 1);
773 break;
774 case NS_VK_END:
775 if (shouldSelect) {
776 AdjustIndexForDisabledOpt(int32_t(numOptions) - 1, newIndex,
777 int32_t(numOptions), 0, -1);
779 break;
780 default: // printable key will be handled by keypress event.
781 incrementalHandler.CancelResetting();
782 return NS_OK;
785 aKeyEvent->PreventDefault();
787 // Actually process the new index and let the selection code
788 // do the scrolling for us
789 PostHandleKeyEvent(newIndex, 0, keyEvent->IsShift(), isControlOrMeta);
790 return NS_OK;
793 HTMLOptionElement* HTMLSelectEventListener::GetCurrentOption() const {
794 // The mEndSelectionIndex is what is currently being selected. Use
795 // the selected index if this is kNothingSelected.
796 int32_t endIndex = GetEndSelectionIndex();
797 int32_t focusedIndex =
798 endIndex == kNothingSelected ? mElement->SelectedIndex() : endIndex;
799 if (focusedIndex != kNothingSelected) {
800 return mElement->Item(AssertedCast<uint32_t>(focusedIndex));
803 // There is no selected option. Return the first non-disabled option, if any.
804 return GetNonDisabledOptionFrom(0);
807 HTMLOptionElement* HTMLSelectEventListener::GetNonDisabledOptionFrom(
808 int32_t aFromIndex, int32_t* aFoundIndex) const {
809 const uint32_t length = mElement->Length();
810 for (uint32_t i = std::max(aFromIndex, 0); i < length; ++i) {
811 if (IsOptionInteractivelySelectable(i)) {
812 if (aFoundIndex) {
813 *aFoundIndex = i;
815 return mElement->Item(i);
818 return nullptr;
821 void HTMLSelectEventListener::PostHandleKeyEvent(int32_t aNewIndex,
822 uint32_t aCharCode,
823 bool aIsShift,
824 bool aIsControlOrMeta) {
825 if (aNewIndex == kNothingSelected) {
826 int32_t endIndex = GetEndSelectionIndex();
827 int32_t focusedIndex =
828 endIndex == kNothingSelected ? mElement->SelectedIndex() : endIndex;
829 if (focusedIndex != kNothingSelected) {
830 return;
832 // No options are selected. In this case the focus ring is on the first
833 // non-disabled option (if any), so we should behave as if that's the option
834 // the user acted on.
835 if (!GetNonDisabledOptionFrom(0, &aNewIndex)) {
836 return;
840 if (mIsCombobox) {
841 RefPtr<HTMLOptionElement> newOption = mElement->Item(aNewIndex);
842 MOZ_ASSERT(newOption);
843 if (newOption->Selected()) {
844 return;
846 newOption->SetSelected(true);
847 FireOnInputAndOnChange();
848 return;
850 if (nsListControlFrame* lf = GetListControlFrame()) {
851 lf->UpdateSelectionAfterKeyEvent(aNewIndex, aCharCode, aIsShift,
852 aIsControlOrMeta, mControlSelectMode);
856 } // namespace mozilla