autocomplete_popup_model.cc revision c407dc5cd9bdc5668497f21b26b09d988ab439de
1// Copyright (c) 2010 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/autocomplete/autocomplete_popup_model.h" 6 7#include "base/string_util.h" 8#include "chrome/browser/autocomplete/autocomplete_edit.h" 9#include "chrome/browser/autocomplete/autocomplete_popup_view.h" 10#include "chrome/browser/profile.h" 11#include "chrome/browser/extensions/extensions_service.h" 12#include "chrome/browser/search_engines/template_url.h" 13#include "chrome/browser/search_engines/template_url_model.h" 14#include "chrome/common/notification_service.h" 15#include "third_party/icu/public/common/unicode/ubidi.h" 16 17/////////////////////////////////////////////////////////////////////////////// 18// AutocompletePopupModel 19 20AutocompletePopupModel::AutocompletePopupModel( 21 AutocompletePopupView* popup_view, 22 AutocompleteEditModel* edit_model, 23 Profile* profile) 24 : view_(popup_view), 25 edit_model_(edit_model), 26 controller_(new AutocompleteController(profile)), 27 profile_(profile), 28 hovered_line_(kNoMatch), 29 selected_line_(kNoMatch) { 30 registrar_.Add(this, NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED, 31 Source<AutocompleteController>(controller_.get())); 32} 33 34AutocompletePopupModel::~AutocompletePopupModel() { 35} 36 37void AutocompletePopupModel::SetProfile(Profile* profile) { 38 DCHECK(profile); 39 profile_ = profile; 40 controller_->SetProfile(profile); 41} 42 43void AutocompletePopupModel::StartAutocomplete( 44 const std::wstring& text, 45 const std::wstring& desired_tld, 46 bool prevent_inline_autocomplete, 47 bool prefer_keyword) { 48 // The user is interacting with the edit, so stop tracking hover. 49 SetHoveredLine(kNoMatch); 50 51 manually_selected_match_.Clear(); 52 53 controller_->Start(text, desired_tld, prevent_inline_autocomplete, 54 prefer_keyword, false); 55} 56 57void AutocompletePopupModel::StopAutocomplete() { 58 controller_->Stop(true); 59} 60 61bool AutocompletePopupModel::IsOpen() const { 62 return view_->IsOpen(); 63} 64 65void AutocompletePopupModel::SetHoveredLine(size_t line) { 66 const bool is_disabling = (line == kNoMatch); 67 DCHECK(is_disabling || (line < controller_->result().size())); 68 69 if (line == hovered_line_) 70 return; // Nothing to do 71 72 // Make sure the old hovered line is redrawn. No need to redraw the selected 73 // line since selection overrides hover so the appearance won't change. 74 if ((hovered_line_ != kNoMatch) && (hovered_line_ != selected_line_)) 75 view_->InvalidateLine(hovered_line_); 76 77 // Change the hover to the new line. 78 hovered_line_ = line; 79 if (!is_disabling && (hovered_line_ != selected_line_)) 80 view_->InvalidateLine(hovered_line_); 81} 82 83void AutocompletePopupModel::SetSelectedLine(size_t line, 84 bool reset_to_default) { 85 const AutocompleteResult& result = controller_->result(); 86 CHECK(line < result.size()); 87 if (result.empty()) 88 return; 89 90 // Cancel the query so the matches don't change on the user. 91 controller_->Stop(false); 92 93 const AutocompleteMatch& match = result.match_at(line); 94 if (reset_to_default) { 95 manually_selected_match_.Clear(); 96 } else { 97 // Track the user's selection until they cancel it. 98 manually_selected_match_.destination_url = match.destination_url; 99 manually_selected_match_.provider_affinity = match.provider; 100 manually_selected_match_.is_history_what_you_typed_match = 101 match.is_history_what_you_typed_match; 102 } 103 104 if (line == selected_line_) 105 return; // Nothing else to do. 106 107 // We need to update |selected_line_| before calling OnPopupDataChanged(), so 108 // that when the edit notifies its controller that something has changed, the 109 // controller can get the correct updated data. 110 // 111 // NOTE: We should never reach here with no selected line; the same code that 112 // opened the popup and made it possible to get here should have also set a 113 // selected line. 114 CHECK(selected_line_ != kNoMatch); 115 GURL current_destination(result.match_at(selected_line_).destination_url); 116 view_->InvalidateLine(selected_line_); 117 selected_line_ = line; 118 view_->InvalidateLine(selected_line_); 119 120 // Update the edit with the new data for this match. 121 // TODO(pkasting): If |selected_line_| moves to the controller, this can be 122 // eliminated and just become a call to the observer on the edit. 123 std::wstring keyword; 124 const bool is_keyword_hint = GetKeywordForMatch(match, &keyword); 125 if (reset_to_default) { 126 std::wstring inline_autocomplete_text; 127 if ((match.inline_autocomplete_offset != std::wstring::npos) && 128 (match.inline_autocomplete_offset < match.fill_into_edit.length())) { 129 inline_autocomplete_text = 130 match.fill_into_edit.substr(match.inline_autocomplete_offset); 131 } 132 edit_model_->OnPopupDataChanged(inline_autocomplete_text, NULL, 133 keyword, is_keyword_hint); 134 } else { 135 edit_model_->OnPopupDataChanged(match.fill_into_edit, ¤t_destination, 136 keyword, is_keyword_hint); 137 } 138 139 // Repaint old and new selected lines immediately, so that the edit doesn't 140 // appear to update [much] faster than the popup. 141 view_->PaintUpdatesNow(); 142} 143 144void AutocompletePopupModel::ResetToDefaultMatch() { 145 const AutocompleteResult& result = controller_->result(); 146 CHECK(!result.empty()); 147 SetSelectedLine(result.default_match() - result.begin(), true); 148 view_->OnDragCanceled(); 149} 150 151void AutocompletePopupModel::InfoForCurrentSelection( 152 AutocompleteMatch* match, 153 GURL* alternate_nav_url) const { 154 DCHECK(match != NULL); 155 const AutocompleteResult* result; 156 if (!controller_->done()) { 157 result = &controller_->latest_result(); 158 // It's technically possible for |result| to be empty if no provider returns 159 // a synchronous result but the query has not completed synchronously; 160 // pratically, however, that should never actually happen. 161 if (result->empty()) 162 return; 163 // The user cannot have manually selected a match, or the query would have 164 // stopped. So the default match must be the desired selection. 165 *match = *result->default_match(); 166 } else { 167 CHECK(IsOpen()); 168 // The query isn't running, so the standard result set can't possibly be out 169 // of date. 170 // 171 // NOTE: In practice, it should actually be safe to use 172 // controller_->latest_result() here too, since the controller keeps that 173 // up-to-date. However we generally try to avoid referring to that. 174 result = &controller_->result(); 175 // If there are no results, the popup should be closed (so we should have 176 // failed the CHECK above), and URLsForDefaultMatch() should have been 177 // called instead. 178 CHECK(!result->empty()); 179 CHECK(selected_line_ < result->size()); 180 *match = result->match_at(selected_line_); 181 } 182 if (alternate_nav_url && manually_selected_match_.empty()) 183 *alternate_nav_url = result->alternate_nav_url(); 184} 185 186bool AutocompletePopupModel::GetKeywordForMatch(const AutocompleteMatch& match, 187 std::wstring* keyword) const { 188 // Assume we have no keyword until we find otherwise. 189 keyword->clear(); 190 191 // If the current match is a keyword, return that as the selected keyword. 192 if (TemplateURL::SupportsReplacement(match.template_url)) { 193 keyword->assign(match.template_url->keyword()); 194 return false; 195 } 196 197 // See if the current match's fill_into_edit corresponds to a keyword. 198 if (!profile_->GetTemplateURLModel()) 199 return false; 200 profile_->GetTemplateURLModel()->Load(); 201 const std::wstring keyword_hint( 202 TemplateURLModel::CleanUserInputKeyword(match.fill_into_edit)); 203 if (keyword_hint.empty()) 204 return false; 205 206 // Don't provide a hint if this keyword doesn't support replacement. 207 const TemplateURL* const template_url = 208 profile_->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword_hint); 209 if (!TemplateURL::SupportsReplacement(template_url)) 210 return false; 211 212 keyword->assign(keyword_hint); 213 return true; 214} 215 216AutocompleteLog* AutocompletePopupModel::GetAutocompleteLog() { 217 return new AutocompleteLog(controller_->input().text(), 218 controller_->input().type(), selected_line_, 0, controller_->result()); 219} 220 221void AutocompletePopupModel::Move(int count) { 222 const AutocompleteResult& result = controller_->result(); 223 if (result.empty()) 224 return; 225 226 // The user is using the keyboard to change the selection, so stop tracking 227 // hover. 228 SetHoveredLine(kNoMatch); 229 230 // Clamp the new line to [0, result_.count() - 1]. 231 const size_t new_line = selected_line_ + count; 232 SetSelectedLine((((count < 0) && (new_line >= selected_line_)) ? 233 0 : std::min(new_line, result.size() - 1)), false); 234} 235 236void AutocompletePopupModel::TryDeletingCurrentItem() { 237 // We could use InfoForCurrentSelection() here, but it seems better to try 238 // and shift-delete the actual selection, rather than any "in progress, not 239 // yet visible" one. 240 if (selected_line_ == kNoMatch) 241 return; 242 243 // Cancel the query so the matches don't change on the user. 244 controller_->Stop(false); 245 246 const AutocompleteMatch& match = 247 controller_->result().match_at(selected_line_); 248 if (match.deletable) { 249 const size_t selected_line = selected_line_; 250 controller_->DeleteMatch(match); // This may synchronously notify us that 251 // the results have changed. 252 const AutocompleteResult& result = controller_->result(); 253 if (!result.empty()) { 254 // Move the selection to the next choice after the deleted one. 255 // TODO(pkasting): Eventually the controller should take care of this 256 // before notifying us, reducing flicker. At that point the check for 257 // deletability can move there too. 258 SetSelectedLine(std::min(result.size() - 1, selected_line), false); 259 } 260 } 261} 262 263void AutocompletePopupModel::Observe(NotificationType type, 264 const NotificationSource& source, 265 const NotificationDetails& details) { 266 DCHECK_EQ(NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED, 267 type.value); 268 269 const AutocompleteResult* result = 270 Details<const AutocompleteResult>(details).ptr(); 271 selected_line_ = (result->default_match() == result->end()) ? 272 kNoMatch : (result->default_match() - result->begin()); 273 // There had better not be a nonempty result set with no default match. 274 CHECK((selected_line_ != kNoMatch) || result->empty()); 275 // If we're going to trim the window size to no longer include the hovered 276 // line, turn hover off. Practically, this shouldn't happen, but it 277 // doesn't hurt to be defensive. 278 if ((hovered_line_ != kNoMatch) && (result->size() <= hovered_line_)) 279 SetHoveredLine(kNoMatch); 280 281 view_->UpdatePopupAppearance(); 282} 283 284const SkBitmap* AutocompletePopupModel::GetSpecialIconForMatch( 285 const AutocompleteMatch& match) const { 286 if (!match.template_url || !match.template_url->IsExtensionKeyword()) 287 return NULL; 288 289 return &profile_->GetExtensionsService()->GetOmniboxIcon( 290 match.template_url->GetExtensionId()); 291} 292