michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "nsAutoCompleteController.h" michael@0: #include "nsAutoCompleteSimpleResult.h" michael@0: michael@0: #include "nsAutoPtr.h" michael@0: #include "nsNetCID.h" michael@0: #include "nsIIOService.h" michael@0: #include "nsToolkitCompsCID.h" michael@0: #include "nsIServiceManager.h" michael@0: #include "nsIAtomService.h" michael@0: #include "nsReadableUtils.h" michael@0: #include "nsUnicharUtils.h" michael@0: #include "nsITreeBoxObject.h" michael@0: #include "nsITreeColumns.h" michael@0: #include "nsIObserverService.h" michael@0: #include "nsIDOMKeyEvent.h" michael@0: #include "mozilla/Services.h" michael@0: #include "mozilla/ModuleUtils.h" michael@0: michael@0: static const char *kAutoCompleteSearchCID = "@mozilla.org/autocomplete/search;1?name="; michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_CLASS(nsAutoCompleteController) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsAutoCompleteController) michael@0: tmp->SetInput(nullptr); michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_END michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsAutoCompleteController) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInput) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSearches) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResults) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTING_ADDREF(nsAutoCompleteController) michael@0: NS_IMPL_CYCLE_COLLECTING_RELEASE(nsAutoCompleteController) michael@0: NS_INTERFACE_TABLE_HEAD(nsAutoCompleteController) michael@0: NS_INTERFACE_TABLE(nsAutoCompleteController, nsIAutoCompleteController, michael@0: nsIAutoCompleteObserver, nsITimerCallback, nsITreeView) michael@0: NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(nsAutoCompleteController) michael@0: NS_INTERFACE_MAP_END michael@0: michael@0: nsAutoCompleteController::nsAutoCompleteController() : michael@0: mDefaultIndexCompleted(false), michael@0: mBackspaced(false), michael@0: mPopupClosedByCompositionStart(false), michael@0: mCompositionState(eCompositionState_None), michael@0: mSearchStatus(nsAutoCompleteController::STATUS_NONE), michael@0: mRowCount(0), michael@0: mSearchesOngoing(0), michael@0: mSearchesFailed(0), michael@0: mFirstSearchResult(false), michael@0: mImmediateSearchesCount(0) michael@0: { michael@0: } michael@0: michael@0: nsAutoCompleteController::~nsAutoCompleteController() michael@0: { michael@0: SetInput(nullptr); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////// michael@0: //// nsIAutoCompleteController michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetSearchStatus(uint16_t *aSearchStatus) michael@0: { michael@0: *aSearchStatus = mSearchStatus; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetMatchCount(uint32_t *aMatchCount) michael@0: { michael@0: *aMatchCount = mRowCount; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetInput(nsIAutoCompleteInput **aInput) michael@0: { michael@0: *aInput = mInput; michael@0: NS_IF_ADDREF(*aInput); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::SetInput(nsIAutoCompleteInput *aInput) michael@0: { michael@0: // Don't do anything if the input isn't changing. michael@0: if (mInput == aInput) michael@0: return NS_OK; michael@0: michael@0: // Clear out the current search context michael@0: if (mInput) { michael@0: // Stop all searches in case they are async. michael@0: StopSearch(); michael@0: ClearResults(); michael@0: ClosePopup(); michael@0: mSearches.Clear(); michael@0: } michael@0: michael@0: mInput = aInput; michael@0: michael@0: // Nothing more to do if the input was just being set to null. michael@0: if (!aInput) michael@0: return NS_OK; michael@0: michael@0: nsAutoString newValue; michael@0: aInput->GetTextValue(newValue); michael@0: michael@0: // Clear out this reference in case the new input's popup has no tree michael@0: mTree = nullptr; michael@0: michael@0: // Reset all search state members to default values michael@0: mSearchString = newValue; michael@0: mDefaultIndexCompleted = false; michael@0: mBackspaced = false; michael@0: mSearchStatus = nsIAutoCompleteController::STATUS_NONE; michael@0: mRowCount = 0; michael@0: mSearchesOngoing = 0; michael@0: michael@0: // Initialize our list of search objects michael@0: uint32_t searchCount; michael@0: aInput->GetSearchCount(&searchCount); michael@0: mResults.SetCapacity(searchCount); michael@0: mSearches.SetCapacity(searchCount); michael@0: mMatchCounts.SetLength(searchCount); michael@0: mImmediateSearchesCount = 0; michael@0: michael@0: const char *searchCID = kAutoCompleteSearchCID; michael@0: michael@0: for (uint32_t i = 0; i < searchCount; ++i) { michael@0: // Use the search name to create the contract id string for the search service michael@0: nsAutoCString searchName; michael@0: aInput->GetSearchAt(i, searchName); michael@0: nsAutoCString cid(searchCID); michael@0: cid.Append(searchName); michael@0: michael@0: // Use the created cid to get a pointer to the search service and store it for later michael@0: nsCOMPtr search = do_GetService(cid.get()); michael@0: if (search) { michael@0: mSearches.AppendObject(search); michael@0: michael@0: // Count immediate searches. michael@0: uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED; michael@0: nsCOMPtr searchDesc = michael@0: do_QueryInterface(search); michael@0: if (searchDesc && NS_SUCCEEDED(searchDesc->GetSearchType(&searchType)) && michael@0: searchType == nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE) michael@0: mImmediateSearchesCount++; michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::StartSearch(const nsAString &aSearchString) michael@0: { michael@0: mSearchString = aSearchString; michael@0: StartSearches(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::HandleText() michael@0: { michael@0: // Note: the events occur in the following order when IME is used. michael@0: // 1. a compositionstart event(HandleStartComposition) michael@0: // 2. some input events (HandleText), eCompositionState_Composing michael@0: // 3. a compositionend event(HandleEndComposition) michael@0: // 4. an input event(HandleText), eCompositionState_Committing michael@0: // We should do nothing during composition. michael@0: if (mCompositionState == eCompositionState_Composing) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: bool handlingCompositionCommit = michael@0: (mCompositionState == eCompositionState_Committing); michael@0: bool popupClosedByCompositionStart = mPopupClosedByCompositionStart; michael@0: if (handlingCompositionCommit) { michael@0: mCompositionState = eCompositionState_None; michael@0: mPopupClosedByCompositionStart = false; michael@0: } michael@0: michael@0: if (!mInput) { michael@0: // Stop all searches in case they are async. michael@0: StopSearch(); michael@0: // Note: if now is after blur and IME end composition, michael@0: // check mInput before calling. michael@0: // See https://bugzilla.mozilla.org/show_bug.cgi?id=193544#c31 michael@0: NS_ERROR("Called before attaching to the control or after detaching from the control"); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsAutoString newValue; michael@0: nsCOMPtr input(mInput); michael@0: input->GetTextValue(newValue); michael@0: michael@0: // Stop all searches in case they are async. michael@0: StopSearch(); michael@0: michael@0: if (!mInput) { michael@0: // StopSearch() can call PostSearchCleanup() which might result michael@0: // in a blur event, which could null out mInput, so we need to check it michael@0: // again. See bug #395344 for more details michael@0: return NS_OK; michael@0: } michael@0: michael@0: bool disabled; michael@0: input->GetDisableAutoComplete(&disabled); michael@0: NS_ENSURE_TRUE(!disabled, NS_OK); michael@0: michael@0: // Don't search again if the new string is the same as the last search michael@0: // However, if this is called immediately after compositionend event, michael@0: // we need to search the same value again since the search was canceled michael@0: // at compositionstart event handler. michael@0: if (!handlingCompositionCommit && newValue.Length() > 0 && michael@0: newValue.Equals(mSearchString)) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Determine if the user has removed text from the end (probably by backspacing) michael@0: if (newValue.Length() < mSearchString.Length() && michael@0: Substring(mSearchString, 0, newValue.Length()).Equals(newValue)) michael@0: { michael@0: // We need to throw away previous results so we don't try to search through them again michael@0: ClearResults(); michael@0: mBackspaced = true; michael@0: } else michael@0: mBackspaced = false; michael@0: michael@0: mSearchString = newValue; michael@0: michael@0: // Don't search if the value is empty michael@0: if (newValue.Length() == 0) { michael@0: // If autocomplete popup was closed by compositionstart event handler, michael@0: // we should reopen it forcibly even if the value is empty. michael@0: if (popupClosedByCompositionStart && handlingCompositionCommit) { michael@0: bool cancel; michael@0: HandleKeyNavigation(nsIDOMKeyEvent::DOM_VK_DOWN, &cancel); michael@0: return NS_OK; michael@0: } michael@0: ClosePopup(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: StartSearches(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::HandleEnter(bool aIsPopupSelection, bool *_retval) michael@0: { michael@0: *_retval = false; michael@0: if (!mInput) michael@0: return NS_OK; michael@0: michael@0: // allow the event through unless there is something selected in the popup michael@0: mInput->GetPopupOpen(_retval); michael@0: if (*_retval) { michael@0: nsCOMPtr popup; michael@0: mInput->GetPopup(getter_AddRefs(popup)); michael@0: michael@0: if (popup) { michael@0: int32_t selectedIndex; michael@0: popup->GetSelectedIndex(&selectedIndex); michael@0: *_retval = selectedIndex >= 0; michael@0: } michael@0: } michael@0: michael@0: // Stop the search, and handle the enter. michael@0: StopSearch(); michael@0: EnterMatch(aIsPopupSelection); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::HandleEscape(bool *_retval) michael@0: { michael@0: *_retval = false; michael@0: if (!mInput) michael@0: return NS_OK; michael@0: michael@0: // allow the event through if the popup is closed michael@0: mInput->GetPopupOpen(_retval); michael@0: michael@0: // Stop all searches in case they are async. michael@0: StopSearch(); michael@0: ClearResults(); michael@0: RevertTextValue(); michael@0: ClosePopup(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::HandleStartComposition() michael@0: { michael@0: NS_ENSURE_TRUE(mCompositionState != eCompositionState_Composing, NS_OK); michael@0: michael@0: mPopupClosedByCompositionStart = false; michael@0: mCompositionState = eCompositionState_Composing; michael@0: michael@0: if (!mInput) michael@0: return NS_OK; michael@0: michael@0: nsCOMPtr input(mInput); michael@0: bool disabled; michael@0: input->GetDisableAutoComplete(&disabled); michael@0: if (disabled) michael@0: return NS_OK; michael@0: michael@0: // Stop all searches in case they are async. michael@0: StopSearch(); michael@0: michael@0: bool isOpen = false; michael@0: input->GetPopupOpen(&isOpen); michael@0: if (isOpen) { michael@0: ClosePopup(); michael@0: michael@0: bool stillOpen = false; michael@0: input->GetPopupOpen(&stillOpen); michael@0: mPopupClosedByCompositionStart = !stillOpen; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::HandleEndComposition() michael@0: { michael@0: NS_ENSURE_TRUE(mCompositionState == eCompositionState_Composing, NS_OK); michael@0: michael@0: // We can't yet retrieve the committed value from the editor, since it isn't michael@0: // completely committed yet. Set mCompositionState to michael@0: // eCompositionState_Committing, so that when HandleText() is called (in michael@0: // response to the "input" event), we know that we should handle the michael@0: // committed text. michael@0: mCompositionState = eCompositionState_Committing; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::HandleTab() michael@0: { michael@0: bool cancel; michael@0: return HandleEnter(false, &cancel); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::HandleKeyNavigation(uint32_t aKey, bool *_retval) michael@0: { michael@0: // By default, don't cancel the event michael@0: *_retval = false; michael@0: michael@0: if (!mInput) { michael@0: // Stop all searches in case they are async. michael@0: StopSearch(); michael@0: // Note: if now is after blur and IME end composition, michael@0: // check mInput before calling. michael@0: // See https://bugzilla.mozilla.org/show_bug.cgi?id=193544#c31 michael@0: NS_ERROR("Called before attaching to the control or after detaching from the control"); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsCOMPtr input(mInput); michael@0: nsCOMPtr popup; michael@0: input->GetPopup(getter_AddRefs(popup)); michael@0: NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE); michael@0: michael@0: bool disabled; michael@0: input->GetDisableAutoComplete(&disabled); michael@0: NS_ENSURE_TRUE(!disabled, NS_OK); michael@0: michael@0: if (aKey == nsIDOMKeyEvent::DOM_VK_UP || michael@0: aKey == nsIDOMKeyEvent::DOM_VK_DOWN || michael@0: aKey == nsIDOMKeyEvent::DOM_VK_PAGE_UP || michael@0: aKey == nsIDOMKeyEvent::DOM_VK_PAGE_DOWN) michael@0: { michael@0: // Prevent the input from handling up/down events, as it may move michael@0: // the cursor to home/end on some systems michael@0: *_retval = true; michael@0: michael@0: bool isOpen = false; michael@0: input->GetPopupOpen(&isOpen); michael@0: if (isOpen) { michael@0: bool reverse = aKey == nsIDOMKeyEvent::DOM_VK_UP || michael@0: aKey == nsIDOMKeyEvent::DOM_VK_PAGE_UP ? true : false; michael@0: bool page = aKey == nsIDOMKeyEvent::DOM_VK_PAGE_UP || michael@0: aKey == nsIDOMKeyEvent::DOM_VK_PAGE_DOWN ? true : false; michael@0: michael@0: // Fill in the value of the textbox with whatever is selected in the popup michael@0: // if the completeSelectedIndex attribute is set. We check this before michael@0: // calling SelectBy of an earlier attempt to avoid crashing. michael@0: bool completeSelection; michael@0: input->GetCompleteSelectedIndex(&completeSelection); michael@0: michael@0: // Instruct the result view to scroll by the given amount and direction michael@0: popup->SelectBy(reverse, page); michael@0: michael@0: if (completeSelection) michael@0: { michael@0: int32_t selectedIndex; michael@0: popup->GetSelectedIndex(&selectedIndex); michael@0: if (selectedIndex >= 0) { michael@0: // A result is selected, so fill in its value michael@0: nsAutoString value; michael@0: if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) { michael@0: input->SetTextValue(value); michael@0: input->SelectTextRange(value.Length(), value.Length()); michael@0: } michael@0: } else { michael@0: // Nothing is selected, so fill in the last typed value michael@0: input->SetTextValue(mSearchString); michael@0: input->SelectTextRange(mSearchString.Length(), mSearchString.Length()); michael@0: } michael@0: } michael@0: } else { michael@0: #ifdef XP_MACOSX michael@0: // on Mac, only show the popup if the caret is at the start or end of michael@0: // the input and there is no selection, so that the default defined key michael@0: // shortcuts for up and down move to the beginning and end of the field michael@0: // otherwise. michael@0: int32_t start, end; michael@0: if (aKey == nsIDOMKeyEvent::DOM_VK_UP) { michael@0: input->GetSelectionStart(&start); michael@0: input->GetSelectionEnd(&end); michael@0: if (start > 0 || start != end) michael@0: *_retval = false; michael@0: } michael@0: else if (aKey == nsIDOMKeyEvent::DOM_VK_DOWN) { michael@0: nsAutoString text; michael@0: input->GetTextValue(text); michael@0: input->GetSelectionStart(&start); michael@0: input->GetSelectionEnd(&end); michael@0: if (start != end || end < (int32_t)text.Length()) michael@0: *_retval = false; michael@0: } michael@0: #endif michael@0: if (*_retval) { michael@0: // Open the popup if there has been a previous search, or else kick off a new search michael@0: if (!mResults.IsEmpty()) { michael@0: if (mRowCount) { michael@0: OpenPopup(); michael@0: } michael@0: } else { michael@0: // Stop all searches in case they are async. michael@0: StopSearch(); michael@0: michael@0: if (!mInput) { michael@0: // StopSearch() can call PostSearchCleanup() which might result michael@0: // in a blur event, which could null out mInput, so we need to check it michael@0: // again. See bug #395344 for more details michael@0: return NS_OK; michael@0: } michael@0: michael@0: StartSearches(); michael@0: } michael@0: } michael@0: } michael@0: } else if ( aKey == nsIDOMKeyEvent::DOM_VK_LEFT michael@0: || aKey == nsIDOMKeyEvent::DOM_VK_RIGHT michael@0: #ifndef XP_MACOSX michael@0: || aKey == nsIDOMKeyEvent::DOM_VK_HOME michael@0: #endif michael@0: ) michael@0: { michael@0: // The user hit a text-navigation key. michael@0: bool isOpen = false; michael@0: input->GetPopupOpen(&isOpen); michael@0: if (isOpen) { michael@0: int32_t selectedIndex; michael@0: popup->GetSelectedIndex(&selectedIndex); michael@0: bool shouldComplete; michael@0: input->GetCompleteDefaultIndex(&shouldComplete); michael@0: if (selectedIndex >= 0) { michael@0: // The pop-up is open and has a selection, take its value michael@0: nsAutoString value; michael@0: if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) { michael@0: input->SetTextValue(value); michael@0: input->SelectTextRange(value.Length(), value.Length()); michael@0: } michael@0: } michael@0: else if (shouldComplete) { michael@0: // We usually try to preserve the casing of what user has typed, but michael@0: // if he wants to autocomplete, we will replace the value with the michael@0: // actual autocomplete result. michael@0: // The user wants explicitely to use that result, so this ensures michael@0: // association of the result with the autocompleted text. michael@0: nsAutoString value; michael@0: nsAutoString inputValue; michael@0: input->GetTextValue(inputValue); michael@0: if (NS_SUCCEEDED(GetDefaultCompleteValue(-1, false, value)) && michael@0: value.Equals(inputValue, nsCaseInsensitiveStringComparator())) { michael@0: input->SetTextValue(value); michael@0: input->SelectTextRange(value.Length(), value.Length()); michael@0: } michael@0: } michael@0: // Close the pop-up even if nothing was selected michael@0: ClearSearchTimer(); michael@0: ClosePopup(); michael@0: } michael@0: // Update last-searched string to the current input, since the input may michael@0: // have changed. Without this, subsequent backspaces look like text michael@0: // additions, not text deletions. michael@0: nsAutoString value; michael@0: input->GetTextValue(value); michael@0: mSearchString = value; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::HandleDelete(bool *_retval) michael@0: { michael@0: *_retval = false; michael@0: if (!mInput) michael@0: return NS_OK; michael@0: michael@0: nsCOMPtr input(mInput); michael@0: bool isOpen = false; michael@0: input->GetPopupOpen(&isOpen); michael@0: if (!isOpen || mRowCount <= 0) { michael@0: // Nothing left to delete, proceed as normal michael@0: HandleText(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsCOMPtr popup; michael@0: input->GetPopup(getter_AddRefs(popup)); michael@0: michael@0: int32_t index, searchIndex, rowIndex; michael@0: popup->GetSelectedIndex(&index); michael@0: if (index == -1) { michael@0: // No row is selected in the list michael@0: HandleText(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: RowIndexToSearch(index, &searchIndex, &rowIndex); michael@0: NS_ENSURE_TRUE(searchIndex >= 0 && rowIndex >= 0, NS_ERROR_FAILURE); michael@0: michael@0: nsIAutoCompleteResult *result = mResults[searchIndex]; michael@0: NS_ENSURE_TRUE(result, NS_ERROR_FAILURE); michael@0: michael@0: nsAutoString search; michael@0: input->GetSearchParam(search); michael@0: michael@0: // Clear the row in our result and in the DB. michael@0: result->RemoveValueAt(rowIndex, true); michael@0: --mRowCount; michael@0: michael@0: // We removed it, so make sure we cancel the event that triggered this call. michael@0: *_retval = true; michael@0: michael@0: // Unselect the current item. michael@0: popup->SetSelectedIndex(-1); michael@0: michael@0: // Tell the tree that the row count changed. michael@0: if (mTree) michael@0: mTree->RowCountChanged(mRowCount, -1); michael@0: michael@0: // Adjust index, if needed. michael@0: if (index >= (int32_t)mRowCount) michael@0: index = mRowCount - 1; michael@0: michael@0: if (mRowCount > 0) { michael@0: // There are still rows in the popup, select the current index again. michael@0: popup->SetSelectedIndex(index); michael@0: michael@0: // Complete to the new current value. michael@0: bool shouldComplete = false; michael@0: input->GetCompleteDefaultIndex(&shouldComplete); michael@0: if (shouldComplete) { michael@0: nsAutoString value; michael@0: if (NS_SUCCEEDED(GetResultValueAt(index, false, value))) { michael@0: CompleteValue(value); michael@0: } michael@0: } michael@0: michael@0: // Invalidate the popup. michael@0: popup->Invalidate(); michael@0: } else { michael@0: // Nothing left in the popup, clear any pending search timers and michael@0: // close the popup. michael@0: ClearSearchTimer(); michael@0: uint32_t minResults; michael@0: input->GetMinResultsForPopup(&minResults); michael@0: if (minResults) { michael@0: ClosePopup(); michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::GetResultAt(int32_t aIndex, nsIAutoCompleteResult** aResult, michael@0: int32_t* aRowIndex) michael@0: { michael@0: int32_t searchIndex; michael@0: RowIndexToSearch(aIndex, &searchIndex, aRowIndex); michael@0: NS_ENSURE_TRUE(searchIndex >= 0 && *aRowIndex >= 0, NS_ERROR_FAILURE); michael@0: michael@0: *aResult = mResults[searchIndex]; michael@0: NS_ENSURE_TRUE(*aResult, NS_ERROR_FAILURE); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetValueAt(int32_t aIndex, nsAString & _retval) michael@0: { michael@0: GetResultLabelAt(aIndex, _retval); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetLabelAt(int32_t aIndex, nsAString & _retval) michael@0: { michael@0: GetResultLabelAt(aIndex, _retval); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetCommentAt(int32_t aIndex, nsAString & _retval) michael@0: { michael@0: int32_t rowIndex; michael@0: nsIAutoCompleteResult* result; michael@0: nsresult rv = GetResultAt(aIndex, &result, &rowIndex); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return result->GetCommentAt(rowIndex, _retval); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetStyleAt(int32_t aIndex, nsAString & _retval) michael@0: { michael@0: int32_t rowIndex; michael@0: nsIAutoCompleteResult* result; michael@0: nsresult rv = GetResultAt(aIndex, &result, &rowIndex); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return result->GetStyleAt(rowIndex, _retval); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetImageAt(int32_t aIndex, nsAString & _retval) michael@0: { michael@0: int32_t rowIndex; michael@0: nsIAutoCompleteResult* result; michael@0: nsresult rv = GetResultAt(aIndex, &result, &rowIndex); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return result->GetImageAt(rowIndex, _retval); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetFinalCompleteValueAt(int32_t aIndex, michael@0: nsAString & _retval) michael@0: { michael@0: int32_t rowIndex; michael@0: nsIAutoCompleteResult* result; michael@0: nsresult rv = GetResultAt(aIndex, &result, &rowIndex); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return result->GetFinalCompleteValueAt(rowIndex, _retval); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::SetSearchString(const nsAString &aSearchString) michael@0: { michael@0: mSearchString = aSearchString; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetSearchString(nsAString &aSearchString) michael@0: { michael@0: aSearchString = mSearchString; michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////// michael@0: //// nsIAutoCompleteObserver michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::OnUpdateSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult) michael@0: { michael@0: ClearResults(); michael@0: return OnSearchResult(aSearch, aResult); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult) michael@0: { michael@0: // look up the index of the search which is returning michael@0: for (uint32_t i = 0; i < mSearches.Length(); ++i) { michael@0: if (mSearches[i] == aSearch) { michael@0: ProcessResult(i, aResult); michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////// michael@0: //// nsITimerCallback michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::Notify(nsITimer *timer) michael@0: { michael@0: mTimer = nullptr; michael@0: michael@0: if (mImmediateSearchesCount == 0) { michael@0: // If there were no immediate searches, BeforeSearches has not yet been michael@0: // called, so do it now. michael@0: nsresult rv = BeforeSearches(); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: } michael@0: StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED); michael@0: AfterSearches(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////// michael@0: // nsITreeView michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetRowCount(int32_t *aRowCount) michael@0: { michael@0: *aRowCount = mRowCount; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetRowProperties(int32_t index, nsAString& aProps) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetCellProperties(int32_t row, nsITreeColumn* col, michael@0: nsAString& aProps) michael@0: { michael@0: if (row >= 0) { michael@0: GetStyleAt(row, aProps); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetColumnProperties(nsITreeColumn* col, nsAString& aProps) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetImageSrc(int32_t row, nsITreeColumn* col, nsAString& _retval) michael@0: { michael@0: const char16_t* colID; michael@0: col->GetIdConst(&colID); michael@0: michael@0: if (NS_LITERAL_STRING("treecolAutoCompleteValue").Equals(colID)) michael@0: return GetImageAt(row, _retval); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetProgressMode(int32_t row, nsITreeColumn* col, int32_t* _retval) michael@0: { michael@0: NS_NOTREACHED("tree has no progress cells"); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetCellValue(int32_t row, nsITreeColumn* col, nsAString& _retval) michael@0: { michael@0: NS_NOTREACHED("all of our cells are text"); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetCellText(int32_t row, nsITreeColumn* col, nsAString& _retval) michael@0: { michael@0: const char16_t* colID; michael@0: col->GetIdConst(&colID); michael@0: michael@0: if (NS_LITERAL_STRING("treecolAutoCompleteValue").Equals(colID)) michael@0: GetValueAt(row, _retval); michael@0: else if (NS_LITERAL_STRING("treecolAutoCompleteComment").Equals(colID)) michael@0: GetCommentAt(row, _retval); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::IsContainer(int32_t index, bool *_retval) michael@0: { michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::IsContainerOpen(int32_t index, bool *_retval) michael@0: { michael@0: NS_NOTREACHED("no container cells"); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::IsContainerEmpty(int32_t index, bool *_retval) michael@0: { michael@0: NS_NOTREACHED("no container cells"); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetLevel(int32_t index, int32_t *_retval) michael@0: { michael@0: *_retval = 0; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetParentIndex(int32_t rowIndex, int32_t *_retval) michael@0: { michael@0: *_retval = -1; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::HasNextSibling(int32_t rowIndex, int32_t afterIndex, bool *_retval) michael@0: { michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::ToggleOpenState(int32_t index) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::SetTree(nsITreeBoxObject *tree) michael@0: { michael@0: mTree = tree; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::GetSelection(nsITreeSelection * *aSelection) michael@0: { michael@0: *aSelection = mSelection; michael@0: NS_IF_ADDREF(*aSelection); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP nsAutoCompleteController::SetSelection(nsITreeSelection * aSelection) michael@0: { michael@0: mSelection = aSelection; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::SelectionChanged() michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::SetCellValue(int32_t row, nsITreeColumn* col, const nsAString& value) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::SetCellText(int32_t row, nsITreeColumn* col, const nsAString& value) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::CycleHeader(nsITreeColumn* col) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::CycleCell(int32_t row, nsITreeColumn* col) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::IsEditable(int32_t row, nsITreeColumn* col, bool *_retval) michael@0: { michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::IsSelectable(int32_t row, nsITreeColumn* col, bool *_retval) michael@0: { michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::IsSeparator(int32_t index, bool *_retval) michael@0: { michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::IsSorted(bool *_retval) michael@0: { michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::CanDrop(int32_t index, int32_t orientation, michael@0: nsIDOMDataTransfer* dataTransfer, bool *_retval) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::Drop(int32_t row, int32_t orientation, nsIDOMDataTransfer* dataTransfer) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::PerformAction(const char16_t *action) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::PerformActionOnRow(const char16_t *action, int32_t row) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::PerformActionOnCell(const char16_t* action, int32_t row, nsITreeColumn* col) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////// michael@0: //// nsAutoCompleteController michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::OpenPopup() michael@0: { michael@0: uint32_t minResults; michael@0: mInput->GetMinResultsForPopup(&minResults); michael@0: michael@0: if (mRowCount >= minResults) { michael@0: return mInput->SetPopupOpen(true); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::ClosePopup() michael@0: { michael@0: if (!mInput) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: bool isOpen = false; michael@0: mInput->GetPopupOpen(&isOpen); michael@0: if (!isOpen) michael@0: return NS_OK; michael@0: michael@0: nsCOMPtr popup; michael@0: mInput->GetPopup(getter_AddRefs(popup)); michael@0: NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE); michael@0: popup->SetSelectedIndex(-1); michael@0: return mInput->SetPopupOpen(false); michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::BeforeSearches() michael@0: { michael@0: NS_ENSURE_STATE(mInput); michael@0: michael@0: mSearchStatus = nsIAutoCompleteController::STATUS_SEARCHING; michael@0: mDefaultIndexCompleted = false; michael@0: michael@0: // The first search result will clear mResults array, though we should pass michael@0: // the previous result to each search to allow them to reuse it. So we michael@0: // temporarily cache current results till AfterSearches(). michael@0: if (!mResultCache.AppendObjects(mResults)) { michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: michael@0: mSearchesOngoing = mSearches.Length(); michael@0: mSearchesFailed = 0; michael@0: mFirstSearchResult = true; michael@0: michael@0: // notify the input that the search is beginning michael@0: mInput->OnSearchBegin(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::StartSearch(uint16_t aSearchType) michael@0: { michael@0: NS_ENSURE_STATE(mInput); michael@0: nsCOMPtr input = mInput; michael@0: michael@0: for (uint32_t i = 0; i < mSearches.Length(); ++i) { michael@0: nsCOMPtr search = mSearches[i]; michael@0: michael@0: // Filter on search type. Not all the searches implement this interface, michael@0: // in such a case just consider them delayed. michael@0: uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED; michael@0: nsCOMPtr searchDesc = michael@0: do_QueryInterface(search); michael@0: if (searchDesc) michael@0: searchDesc->GetSearchType(&searchType); michael@0: if (searchType != aSearchType) michael@0: continue; michael@0: michael@0: nsIAutoCompleteResult *result = mResultCache.SafeObjectAt(i); michael@0: michael@0: if (result) { michael@0: uint16_t searchResult; michael@0: result->GetSearchResult(&searchResult); michael@0: if (searchResult != nsIAutoCompleteResult::RESULT_SUCCESS && michael@0: searchResult != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING && michael@0: searchResult != nsIAutoCompleteResult::RESULT_NOMATCH) michael@0: result = nullptr; michael@0: } michael@0: michael@0: nsAutoString searchParam; michael@0: nsresult rv = input->GetSearchParam(searchParam); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: rv = search->StartSearch(mSearchString, searchParam, result, static_cast(this)); michael@0: if (NS_FAILED(rv)) { michael@0: ++mSearchesFailed; michael@0: --mSearchesOngoing; michael@0: } michael@0: // Because of the joy of nested event loops (which can easily happen when some michael@0: // code uses a generator for an asynchronous AutoComplete search), michael@0: // nsIAutoCompleteSearch::StartSearch might cause us to be detached from our input michael@0: // field. The next time we iterate, we'd be touching something that we shouldn't michael@0: // be, and result in a crash. michael@0: if (!mInput) { michael@0: // The search operation has been finished. michael@0: return NS_OK; michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: nsAutoCompleteController::AfterSearches() michael@0: { michael@0: mResultCache.Clear(); michael@0: if (mSearchesFailed == mSearches.Length()) michael@0: PostSearchCleanup(); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsAutoCompleteController::StopSearch() michael@0: { michael@0: // Stop the timer if there is one michael@0: ClearSearchTimer(); michael@0: michael@0: // Stop any ongoing asynchronous searches michael@0: if (mSearchStatus == nsIAutoCompleteController::STATUS_SEARCHING) { michael@0: for (uint32_t i = 0; i < mSearches.Length(); ++i) { michael@0: nsCOMPtr search = mSearches[i]; michael@0: search->StopSearch(); michael@0: } michael@0: mSearchesOngoing = 0; michael@0: // since we were searching, but now we've stopped, michael@0: // we need to call PostSearchCleanup() michael@0: PostSearchCleanup(); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::StartSearches() michael@0: { michael@0: // Don't create a new search timer if we're already waiting for one to fire. michael@0: // If we don't check for this, we won't be able to cancel the original timer michael@0: // and may crash when it fires (bug 236659). michael@0: if (mTimer || !mInput) michael@0: return NS_OK; michael@0: michael@0: // Get the timeout for delayed searches. michael@0: uint32_t timeout; michael@0: mInput->GetTimeout(&timeout); michael@0: michael@0: uint32_t immediateSearchesCount = mImmediateSearchesCount; michael@0: if (timeout == 0) { michael@0: // All the searches should be executed immediately. michael@0: immediateSearchesCount = mSearches.Length(); michael@0: } michael@0: michael@0: if (immediateSearchesCount > 0) { michael@0: nsresult rv = BeforeSearches(); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE); michael@0: michael@0: if (mSearches.Length() == immediateSearchesCount) { michael@0: // Either all searches are immediate, or the timeout is 0. In the michael@0: // latter case we still have to execute the delayed searches, otherwise michael@0: // this will be a no-op. michael@0: StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED); michael@0: michael@0: // All the searches have been started, just finish. michael@0: AfterSearches(); michael@0: return NS_OK; michael@0: } michael@0: } michael@0: michael@0: MOZ_ASSERT(timeout > 0, "Trying to delay searches with a 0 timeout!"); michael@0: michael@0: // Now start the delayed searches. michael@0: nsresult rv; michael@0: mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: rv = mTimer->InitWithCallback(this, timeout, nsITimer::TYPE_ONE_SHOT); michael@0: if (NS_FAILED(rv)) michael@0: mTimer = nullptr; michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::ClearSearchTimer() michael@0: { michael@0: if (mTimer) { michael@0: mTimer->Cancel(); michael@0: mTimer = nullptr; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::EnterMatch(bool aIsPopupSelection) michael@0: { michael@0: nsCOMPtr input(mInput); michael@0: nsCOMPtr popup; michael@0: input->GetPopup(getter_AddRefs(popup)); michael@0: NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE); michael@0: michael@0: bool forceComplete; michael@0: input->GetForceComplete(&forceComplete); michael@0: michael@0: // Ask the popup if it wants to enter a special value into the textbox michael@0: nsAutoString value; michael@0: popup->GetOverrideValue(value); michael@0: if (value.IsEmpty()) { michael@0: bool shouldComplete; michael@0: input->GetCompleteDefaultIndex(&shouldComplete); michael@0: bool completeSelection; michael@0: input->GetCompleteSelectedIndex(&completeSelection); michael@0: michael@0: int32_t selectedIndex; michael@0: popup->GetSelectedIndex(&selectedIndex); michael@0: if (selectedIndex >= 0) { michael@0: // If completeselectedindex is false or a row was selected from the popup, michael@0: // enter it into the textbox. If completeselectedindex is true, or michael@0: // EnterMatch was called via other means, for instance pressing Enter, michael@0: // don't fill in the value as it will have already been filled in as michael@0: // needed, unless the final complete value differs. michael@0: nsAutoString finalValue, inputValue; michael@0: GetResultValueAt(selectedIndex, true, finalValue); michael@0: input->GetTextValue(inputValue); michael@0: if (!completeSelection || aIsPopupSelection || michael@0: !finalValue.Equals(inputValue)) { michael@0: value = finalValue; michael@0: } michael@0: } michael@0: else if (shouldComplete) { michael@0: // We usually try to preserve the casing of what user has typed, but michael@0: // if he wants to autocomplete, we will replace the value with the michael@0: // actual autocomplete result. michael@0: // The user wants explicitely to use that result, so this ensures michael@0: // association of the result with the autocompleted text. michael@0: nsAutoString defaultIndexValue; michael@0: if (NS_SUCCEEDED(GetFinalDefaultCompleteValue(defaultIndexValue))) michael@0: value = defaultIndexValue; michael@0: } michael@0: michael@0: if (forceComplete && value.IsEmpty()) { michael@0: // Since nothing was selected, and forceComplete is specified, that means michael@0: // we have to find the first default match and enter it instead michael@0: for (uint32_t i = 0; i < mResults.Length(); ++i) { michael@0: nsIAutoCompleteResult *result = mResults[i]; michael@0: michael@0: if (result) { michael@0: int32_t defaultIndex; michael@0: result->GetDefaultIndex(&defaultIndex); michael@0: if (defaultIndex >= 0) { michael@0: result->GetFinalCompleteValueAt(defaultIndex, value); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: nsCOMPtr obsSvc = michael@0: mozilla::services::GetObserverService(); michael@0: NS_ENSURE_STATE(obsSvc); michael@0: obsSvc->NotifyObservers(input, "autocomplete-will-enter-text", nullptr); michael@0: michael@0: if (!value.IsEmpty()) { michael@0: input->SetTextValue(value); michael@0: input->SelectTextRange(value.Length(), value.Length()); michael@0: mSearchString = value; michael@0: } michael@0: michael@0: obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr); michael@0: ClosePopup(); michael@0: michael@0: bool cancel; michael@0: input->OnTextEntered(&cancel); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::RevertTextValue() michael@0: { michael@0: // StopSearch() can call PostSearchCleanup() which might result michael@0: // in a blur event, which could null out mInput, so we need to check it michael@0: // again. See bug #408463 for more details michael@0: if (!mInput) michael@0: return NS_OK; michael@0: michael@0: nsAutoString oldValue(mSearchString); michael@0: nsCOMPtr input(mInput); michael@0: michael@0: bool cancel = false; michael@0: input->OnTextReverted(&cancel); michael@0: michael@0: if (!cancel) { michael@0: nsCOMPtr obsSvc = michael@0: mozilla::services::GetObserverService(); michael@0: NS_ENSURE_STATE(obsSvc); michael@0: obsSvc->NotifyObservers(input, "autocomplete-will-revert-text", nullptr); michael@0: michael@0: nsAutoString inputValue; michael@0: input->GetTextValue(inputValue); michael@0: // Don't change the value if it is the same to prevent sending useless events. michael@0: // NOTE: how can |RevertTextValue| be called with inputValue != oldValue? michael@0: if (!oldValue.Equals(inputValue)) { michael@0: input->SetTextValue(oldValue); michael@0: } michael@0: michael@0: obsSvc->NotifyObservers(input, "autocomplete-did-revert-text", nullptr); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult) michael@0: { michael@0: NS_ENSURE_STATE(mInput); michael@0: nsCOMPtr input(mInput); michael@0: michael@0: // If this is the first search result we are processing michael@0: // we should clear out the previously cached results michael@0: if (mFirstSearchResult) { michael@0: ClearResults(); michael@0: mFirstSearchResult = false; michael@0: } michael@0: michael@0: uint16_t result = 0; michael@0: if (aResult) michael@0: aResult->GetSearchResult(&result); michael@0: michael@0: // if our results are incremental, the search is still ongoing michael@0: if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING && michael@0: result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { michael@0: --mSearchesOngoing; michael@0: } michael@0: michael@0: uint32_t oldMatchCount = 0; michael@0: uint32_t matchCount = 0; michael@0: if (aResult) michael@0: aResult->GetMatchCount(&matchCount); michael@0: michael@0: int32_t resultIndex = mResults.IndexOf(aResult); michael@0: if (resultIndex == -1) { michael@0: // cache the result michael@0: mResults.AppendObject(aResult); michael@0: mMatchCounts.AppendElement(matchCount); michael@0: resultIndex = mResults.Count() - 1; michael@0: } michael@0: else { michael@0: oldMatchCount = mMatchCounts[aSearchIndex]; michael@0: mMatchCounts[resultIndex] = matchCount; michael@0: } michael@0: michael@0: bool isTypeAheadResult = false; michael@0: if (aResult) { michael@0: aResult->GetTypeAheadResult(&isTypeAheadResult); michael@0: } michael@0: michael@0: if (!isTypeAheadResult) { michael@0: uint32_t oldRowCount = mRowCount; michael@0: // If the search failed, increase the match count to include the error michael@0: // description. michael@0: if (result == nsIAutoCompleteResult::RESULT_FAILURE) { michael@0: nsAutoString error; michael@0: aResult->GetErrorDescription(error); michael@0: if (!error.IsEmpty()) { michael@0: ++mRowCount; michael@0: if (mTree) { michael@0: mTree->RowCountChanged(oldRowCount, 1); michael@0: } michael@0: } michael@0: } else if (result == nsIAutoCompleteResult::RESULT_SUCCESS || michael@0: result == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { michael@0: // Increase the match count for all matches in this result. michael@0: mRowCount += matchCount - oldMatchCount; michael@0: michael@0: if (mTree) { michael@0: mTree->RowCountChanged(oldRowCount, matchCount - oldMatchCount); michael@0: } michael@0: } michael@0: michael@0: // Refresh the popup view to display the new search results michael@0: nsCOMPtr popup; michael@0: input->GetPopup(getter_AddRefs(popup)); michael@0: NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE); michael@0: popup->Invalidate(); michael@0: michael@0: uint32_t minResults; michael@0: input->GetMinResultsForPopup(&minResults); michael@0: michael@0: // Make sure the popup is open, if necessary, since we now have at least one michael@0: // search result ready to display. Don't force the popup closed if we might michael@0: // get results in the future to avoid unnecessarily canceling searches. michael@0: if (mRowCount || !minResults) { michael@0: OpenPopup(); michael@0: } else if (result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { michael@0: ClosePopup(); michael@0: } michael@0: } michael@0: michael@0: if (result == nsIAutoCompleteResult::RESULT_SUCCESS || michael@0: result == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { michael@0: // Try to autocomplete the default index for this search. michael@0: CompleteDefaultIndex(resultIndex); michael@0: } michael@0: michael@0: if (mSearchesOngoing == 0) { michael@0: // If this is the last search to return, cleanup. michael@0: PostSearchCleanup(); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::PostSearchCleanup() michael@0: { michael@0: NS_ENSURE_STATE(mInput); michael@0: nsCOMPtr input(mInput); michael@0: michael@0: uint32_t minResults; michael@0: input->GetMinResultsForPopup(&minResults); michael@0: michael@0: if (mRowCount || minResults == 0) { michael@0: OpenPopup(); michael@0: if (mRowCount) michael@0: mSearchStatus = nsIAutoCompleteController::STATUS_COMPLETE_MATCH; michael@0: else michael@0: mSearchStatus = nsIAutoCompleteController::STATUS_COMPLETE_NO_MATCH; michael@0: } else { michael@0: mSearchStatus = nsIAutoCompleteController::STATUS_COMPLETE_NO_MATCH; michael@0: ClosePopup(); michael@0: } michael@0: michael@0: // notify the input that the search is complete michael@0: input->OnSearchComplete(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::ClearResults() michael@0: { michael@0: int32_t oldRowCount = mRowCount; michael@0: mRowCount = 0; michael@0: mResults.Clear(); michael@0: mMatchCounts.Clear(); michael@0: if (oldRowCount != 0) { michael@0: if (mTree) michael@0: mTree->RowCountChanged(0, -oldRowCount); michael@0: else if (mInput) { michael@0: nsCOMPtr popup; michael@0: mInput->GetPopup(getter_AddRefs(popup)); michael@0: NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE); michael@0: // if we had a tree, RowCountChanged() would have cleared the selection michael@0: // when the selected row was removed. But since we don't have a tree, michael@0: // we need to clear the selection manually. michael@0: popup->SetSelectedIndex(-1); michael@0: } michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::CompleteDefaultIndex(int32_t aResultIndex) michael@0: { michael@0: if (mDefaultIndexCompleted || mBackspaced || mSearchString.Length() == 0 || !mInput) michael@0: return NS_OK; michael@0: michael@0: int32_t selectionStart; michael@0: mInput->GetSelectionStart(&selectionStart); michael@0: int32_t selectionEnd; michael@0: mInput->GetSelectionEnd(&selectionEnd); michael@0: michael@0: // Don't try to automatically complete to the first result if there's already michael@0: // a selection or the cursor isn't at the end of the input michael@0: if (selectionEnd != selectionStart || michael@0: selectionEnd != (int32_t)mSearchString.Length()) michael@0: return NS_OK; michael@0: michael@0: bool shouldComplete; michael@0: mInput->GetCompleteDefaultIndex(&shouldComplete); michael@0: if (!shouldComplete) michael@0: return NS_OK; michael@0: michael@0: nsAutoString resultValue; michael@0: if (NS_SUCCEEDED(GetDefaultCompleteValue(aResultIndex, true, resultValue))) michael@0: CompleteValue(resultValue); michael@0: michael@0: mDefaultIndexCompleted = true; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::GetDefaultCompleteResult(int32_t aResultIndex, michael@0: nsIAutoCompleteResult** _result, michael@0: int32_t* _defaultIndex) michael@0: { michael@0: *_defaultIndex = -1; michael@0: int32_t resultIndex = aResultIndex; michael@0: michael@0: // If a result index was not provided, find the first defaultIndex result. michael@0: for (int32_t i = 0; resultIndex < 0 && i < mResults.Count(); ++i) { michael@0: nsIAutoCompleteResult *result = mResults[i]; michael@0: if (result && michael@0: NS_SUCCEEDED(result->GetDefaultIndex(_defaultIndex)) && michael@0: *_defaultIndex >= 0) { michael@0: resultIndex = i; michael@0: } michael@0: } michael@0: NS_ENSURE_TRUE(resultIndex >= 0, NS_ERROR_FAILURE); michael@0: michael@0: *_result = mResults.SafeObjectAt(resultIndex); michael@0: NS_ENSURE_TRUE(*_result, NS_ERROR_FAILURE); michael@0: michael@0: if (*_defaultIndex < 0) { michael@0: // The search must explicitly provide a default index in order michael@0: // for us to be able to complete. michael@0: (*_result)->GetDefaultIndex(_defaultIndex); michael@0: } michael@0: michael@0: if (*_defaultIndex < 0) { michael@0: // We were given a result index, but that result doesn't want to michael@0: // be autocompleted. michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: // If the result wrongly notifies a RESULT_SUCCESS with no matches, or michael@0: // provides a defaultIndex greater than its matchCount, avoid trying to michael@0: // complete to an empty value. michael@0: uint32_t matchCount = 0; michael@0: (*_result)->GetMatchCount(&matchCount); michael@0: // Here defaultIndex is surely non-negative, so can be cast to unsigned. michael@0: if ((uint32_t)(*_defaultIndex) >= matchCount) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::GetDefaultCompleteValue(int32_t aResultIndex, michael@0: bool aPreserveCasing, michael@0: nsAString &_retval) michael@0: { michael@0: nsIAutoCompleteResult *result; michael@0: int32_t defaultIndex = -1; michael@0: nsresult rv = GetDefaultCompleteResult(aResultIndex, &result, &defaultIndex); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: nsAutoString resultValue; michael@0: result->GetValueAt(defaultIndex, resultValue); michael@0: if (aPreserveCasing && michael@0: StringBeginsWith(resultValue, mSearchString, michael@0: nsCaseInsensitiveStringComparator())) { michael@0: // We try to preserve user casing, otherwise we would end up changing michael@0: // the case of what he typed, if we have a result with a different casing. michael@0: // For example if we have result "Test", and user starts writing "tuna", michael@0: // after digiting t, we would convert it to T trying to autocomplete "Test". michael@0: // We will still complete to cased "Test" if the user explicitely choose michael@0: // that result, by either selecting it in the results popup, or with michael@0: // keyboard navigation or if autocompleting in the middle. michael@0: nsAutoString casedResultValue; michael@0: casedResultValue.Assign(mSearchString); michael@0: // Use what the user has typed so far. michael@0: casedResultValue.Append(Substring(resultValue, michael@0: mSearchString.Length(), michael@0: resultValue.Length())); michael@0: _retval = casedResultValue; michael@0: } michael@0: else michael@0: _retval = resultValue; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::GetFinalDefaultCompleteValue(nsAString &_retval) michael@0: { michael@0: MOZ_ASSERT(mInput, "Must have a valid input"); michael@0: nsIAutoCompleteResult *result; michael@0: int32_t defaultIndex = -1; michael@0: nsresult rv = GetDefaultCompleteResult(-1, &result, &defaultIndex); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: result->GetValueAt(defaultIndex, _retval); michael@0: nsAutoString inputValue; michael@0: mInput->GetTextValue(inputValue); michael@0: if (!_retval.Equals(inputValue, nsCaseInsensitiveStringComparator())) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: nsAutoString finalCompleteValue; michael@0: rv = result->GetFinalCompleteValueAt(defaultIndex, finalCompleteValue); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: _retval = finalCompleteValue; michael@0: } michael@0: michael@0: MOZ_ASSERT(FindInReadable(inputValue, _retval, nsCaseInsensitiveStringComparator()), michael@0: "Return value must include input value."); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::CompleteValue(nsString &aValue) michael@0: /* mInput contains mSearchString, which we want to autocomplete to aValue. If michael@0: * selectDifference is true, select the remaining portion of aValue not michael@0: * contained in mSearchString. */ michael@0: { michael@0: MOZ_ASSERT(mInput, "Must have a valid input"); michael@0: const int32_t mSearchStringLength = mSearchString.Length(); michael@0: int32_t endSelect = aValue.Length(); // By default, select all of aValue. michael@0: michael@0: if (aValue.IsEmpty() || michael@0: StringBeginsWith(aValue, mSearchString, michael@0: nsCaseInsensitiveStringComparator())) { michael@0: // aValue is empty (we were asked to clear mInput), or mSearchString michael@0: // matches the beginning of aValue. In either case we can simply michael@0: // autocomplete to aValue. michael@0: mInput->SetTextValue(aValue); michael@0: } else { michael@0: nsresult rv; michael@0: nsCOMPtr ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: nsAutoCString scheme; michael@0: if (NS_SUCCEEDED(ios->ExtractScheme(NS_ConvertUTF16toUTF8(aValue), scheme))) { michael@0: // Trying to autocomplete a URI from somewhere other than the beginning. michael@0: // Only succeed if the missing portion is "http://"; otherwise do not michael@0: // autocomplete. This prevents us from "helpfully" autocompleting to a michael@0: // URI that isn't equivalent to what the user expected. michael@0: const int32_t findIndex = 7; // length of "http://" michael@0: michael@0: if ((endSelect < findIndex + mSearchStringLength) || michael@0: !scheme.LowerCaseEqualsLiteral("http") || michael@0: !Substring(aValue, findIndex, mSearchStringLength).Equals( michael@0: mSearchString, nsCaseInsensitiveStringComparator())) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: mInput->SetTextValue(mSearchString + michael@0: Substring(aValue, mSearchStringLength + findIndex, michael@0: endSelect)); michael@0: michael@0: endSelect -= findIndex; // We're skipping this many characters of aValue. michael@0: } else { michael@0: // Autocompleting something other than a URI from the middle. michael@0: // Use the format "searchstring >> full string" to indicate to the user michael@0: // what we are going to replace their search string with. michael@0: mInput->SetTextValue(mSearchString + NS_LITERAL_STRING(" >> ") + aValue); michael@0: michael@0: endSelect = mSearchString.Length() + 4 + aValue.Length(); michael@0: } michael@0: } michael@0: michael@0: mInput->SelectTextRange(mSearchStringLength, endSelect); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::GetResultLabelAt(int32_t aIndex, nsAString & _retval) michael@0: { michael@0: return GetResultValueLabelAt(aIndex, false, false, _retval); michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::GetResultValueAt(int32_t aIndex, bool aGetFinalValue, michael@0: nsAString & _retval) michael@0: { michael@0: return GetResultValueLabelAt(aIndex, aGetFinalValue, true, _retval); michael@0: } michael@0: michael@0: nsresult michael@0: nsAutoCompleteController::GetResultValueLabelAt(int32_t aIndex, michael@0: bool aGetFinalValue, michael@0: bool aGetValue, michael@0: nsAString & _retval) michael@0: { michael@0: NS_ENSURE_TRUE(aIndex >= 0 && (uint32_t) aIndex < mRowCount, NS_ERROR_ILLEGAL_VALUE); michael@0: michael@0: int32_t rowIndex; michael@0: nsIAutoCompleteResult *result; michael@0: nsresult rv = GetResultAt(aIndex, &result, &rowIndex); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: uint16_t searchResult; michael@0: result->GetSearchResult(&searchResult); michael@0: michael@0: if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) { michael@0: if (aGetValue) michael@0: return NS_ERROR_FAILURE; michael@0: result->GetErrorDescription(_retval); michael@0: } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS || michael@0: searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { michael@0: if (aGetFinalValue) michael@0: result->GetFinalCompleteValueAt(rowIndex, _retval); michael@0: else if (aGetValue) michael@0: result->GetValueAt(rowIndex, _retval); michael@0: else michael@0: result->GetLabelAt(rowIndex, _retval); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: /** michael@0: * Given the index of a row in the autocomplete popup, find the michael@0: * corresponding nsIAutoCompleteSearch index, and sub-index into michael@0: * the search's results list. michael@0: */ michael@0: nsresult michael@0: nsAutoCompleteController::RowIndexToSearch(int32_t aRowIndex, int32_t *aSearchIndex, int32_t *aItemIndex) michael@0: { michael@0: *aSearchIndex = -1; michael@0: *aItemIndex = -1; michael@0: michael@0: uint32_t index = 0; michael@0: michael@0: // Move index through the results of each registered nsIAutoCompleteSearch michael@0: // until we find the given row michael@0: for (uint32_t i = 0; i < mSearches.Length(); ++i) { michael@0: nsIAutoCompleteResult *result = mResults.SafeObjectAt(i); michael@0: if (!result) michael@0: continue; michael@0: michael@0: uint32_t rowCount = 0; michael@0: michael@0: // Skip past the result completely if it is marked as hidden michael@0: bool isTypeAheadResult = false; michael@0: result->GetTypeAheadResult(&isTypeAheadResult); michael@0: michael@0: if (!isTypeAheadResult) { michael@0: uint16_t searchResult; michael@0: result->GetSearchResult(&searchResult); michael@0: michael@0: // Find out how many results were provided by the michael@0: // current nsIAutoCompleteSearch. michael@0: if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS || michael@0: searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { michael@0: result->GetMatchCount(&rowCount); michael@0: } michael@0: } michael@0: michael@0: // If the given row index is within the results range michael@0: // of the current nsIAutoCompleteSearch then return the michael@0: // search index and sub-index into the results array michael@0: if ((rowCount != 0) && (index + rowCount-1 >= (uint32_t) aRowIndex)) { michael@0: *aSearchIndex = i; michael@0: *aItemIndex = aRowIndex - index; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Advance the popup table index cursor past the michael@0: // results of the current search. michael@0: index += rowCount; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_GENERIC_FACTORY_CONSTRUCTOR(nsAutoCompleteController) michael@0: NS_GENERIC_FACTORY_CONSTRUCTOR(nsAutoCompleteSimpleResult) michael@0: michael@0: NS_DEFINE_NAMED_CID(NS_AUTOCOMPLETECONTROLLER_CID); michael@0: NS_DEFINE_NAMED_CID(NS_AUTOCOMPLETESIMPLERESULT_CID); michael@0: michael@0: static const mozilla::Module::CIDEntry kAutoCompleteCIDs[] = { michael@0: { &kNS_AUTOCOMPLETECONTROLLER_CID, false, nullptr, nsAutoCompleteControllerConstructor }, michael@0: { &kNS_AUTOCOMPLETESIMPLERESULT_CID, false, nullptr, nsAutoCompleteSimpleResultConstructor }, michael@0: { nullptr } michael@0: }; michael@0: michael@0: static const mozilla::Module::ContractIDEntry kAutoCompleteContracts[] = { michael@0: { NS_AUTOCOMPLETECONTROLLER_CONTRACTID, &kNS_AUTOCOMPLETECONTROLLER_CID }, michael@0: { NS_AUTOCOMPLETESIMPLERESULT_CONTRACTID, &kNS_AUTOCOMPLETESIMPLERESULT_CID }, michael@0: { nullptr } michael@0: }; michael@0: michael@0: static const mozilla::Module kAutoCompleteModule = { michael@0: mozilla::Module::kVersion, michael@0: kAutoCompleteCIDs, michael@0: kAutoCompleteContracts michael@0: }; michael@0: michael@0: NSMODULE_DEFN(tkAutoCompleteModule) = &kAutoCompleteModule;