1 // Copyright (c) 2012 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/ui/omnibox/omnibox_popup_model.h" 6 7 #include <algorithm> 8 9 #include "base/strings/string_util.h" 10 #include "base/strings/utf_string_conversions.h" 11 #include "chrome/browser/autocomplete/autocomplete_match.h" 12 #include "chrome/browser/extensions/api/omnibox/omnibox_api.h" 13 #include "chrome/browser/profiles/profile.h" 14 #include "chrome/browser/search_engines/template_url.h" 15 #include "chrome/browser/search_engines/template_url_service.h" 16 #include "chrome/browser/search_engines/template_url_service_factory.h" 17 #include "chrome/browser/ui/omnibox/omnibox_popup_model_observer.h" 18 #include "chrome/browser/ui/omnibox/omnibox_popup_view.h" 19 #include "third_party/icu/source/common/unicode/ubidi.h" 20 #include "ui/gfx/image/image.h" 21 #include "ui/gfx/rect.h" 22 23 /////////////////////////////////////////////////////////////////////////////// 24 // OmniboxPopupModel 25 26 const size_t OmniboxPopupModel::kNoMatch = -1; 27 28 OmniboxPopupModel::OmniboxPopupModel( 29 OmniboxPopupView* popup_view, 30 OmniboxEditModel* edit_model) 31 : view_(popup_view), 32 edit_model_(edit_model), 33 hovered_line_(kNoMatch), 34 selected_line_(kNoMatch), 35 selected_line_state_(NORMAL) { 36 edit_model->set_popup_model(this); 37 } 38 39 OmniboxPopupModel::~OmniboxPopupModel() { 40 } 41 42 bool OmniboxPopupModel::IsOpen() const { 43 return view_->IsOpen(); 44 } 45 46 void OmniboxPopupModel::SetHoveredLine(size_t line) { 47 const bool is_disabling = (line == kNoMatch); 48 DCHECK(is_disabling || (line < result().size())); 49 50 if (line == hovered_line_) 51 return; // Nothing to do 52 53 // Make sure the old hovered line is redrawn. No need to redraw the selected 54 // line since selection overrides hover so the appearance won't change. 55 if ((hovered_line_ != kNoMatch) && (hovered_line_ != selected_line_)) 56 view_->InvalidateLine(hovered_line_); 57 58 // Change the hover to the new line. 59 hovered_line_ = line; 60 if (!is_disabling && (hovered_line_ != selected_line_)) 61 view_->InvalidateLine(hovered_line_); 62 } 63 64 void OmniboxPopupModel::SetSelectedLine(size_t line, 65 bool reset_to_default, 66 bool force) { 67 const AutocompleteResult& result = this->result(); 68 if (result.empty()) 69 return; 70 71 // Cancel the query so the matches don't change on the user. 72 autocomplete_controller()->Stop(false); 73 74 line = std::min(line, result.size() - 1); 75 const AutocompleteMatch& match = result.match_at(line); 76 if (reset_to_default) { 77 manually_selected_match_.Clear(); 78 } else { 79 // Track the user's selection until they cancel it. 80 manually_selected_match_.destination_url = match.destination_url; 81 manually_selected_match_.provider_affinity = match.provider; 82 manually_selected_match_.is_history_what_you_typed_match = 83 match.is_history_what_you_typed_match; 84 } 85 86 if (line == selected_line_ && !force) 87 return; // Nothing else to do. 88 89 // We need to update |selected_line_state_| and |selected_line_| before 90 // calling InvalidateLine(), since it will check them to determine how to 91 // draw. We also need to update |selected_line_| before calling 92 // OnPopupDataChanged(), so that when the edit notifies its controller that 93 // something has changed, the controller can get the correct updated data. 94 // 95 // NOTE: We should never reach here with no selected line; the same code that 96 // opened the popup and made it possible to get here should have also set a 97 // selected line. 98 CHECK(selected_line_ != kNoMatch); 99 GURL current_destination(result.match_at(selected_line_).destination_url); 100 const size_t prev_selected_line = selected_line_; 101 selected_line_state_ = NORMAL; 102 selected_line_ = line; 103 view_->InvalidateLine(prev_selected_line); 104 view_->InvalidateLine(selected_line_); 105 106 // Update the edit with the new data for this match. 107 // TODO(pkasting): If |selected_line_| moves to the controller, this can be 108 // eliminated and just become a call to the observer on the edit. 109 base::string16 keyword; 110 bool is_keyword_hint; 111 match.GetKeywordUIState(edit_model_->profile(), &keyword, &is_keyword_hint); 112 113 if (reset_to_default) { 114 edit_model_->OnPopupDataChanged(match.inline_autocompletion, NULL, 115 keyword, is_keyword_hint); 116 } else { 117 edit_model_->OnPopupDataChanged(match.fill_into_edit, ¤t_destination, 118 keyword, is_keyword_hint); 119 } 120 121 // Repaint old and new selected lines immediately, so that the edit doesn't 122 // appear to update [much] faster than the popup. 123 view_->PaintUpdatesNow(); 124 } 125 126 void OmniboxPopupModel::ResetToDefaultMatch() { 127 const AutocompleteResult& result = this->result(); 128 CHECK(!result.empty()); 129 SetSelectedLine(result.default_match() - result.begin(), true, false); 130 view_->OnDragCanceled(); 131 } 132 133 void OmniboxPopupModel::Move(int count) { 134 const AutocompleteResult& result = this->result(); 135 if (result.empty()) 136 return; 137 138 // The user is using the keyboard to change the selection, so stop tracking 139 // hover. 140 SetHoveredLine(kNoMatch); 141 142 // Clamp the new line to [0, result_.count() - 1]. 143 const size_t new_line = selected_line_ + count; 144 SetSelectedLine(((count < 0) && (new_line >= selected_line_)) ? 0 : new_line, 145 false, false); 146 } 147 148 void OmniboxPopupModel::SetSelectedLineState(LineState state) { 149 DCHECK(!result().empty()); 150 DCHECK_NE(kNoMatch, selected_line_); 151 152 const AutocompleteMatch& match = result().match_at(selected_line_); 153 DCHECK(match.associated_keyword.get()); 154 155 selected_line_state_ = state; 156 view_->InvalidateLine(selected_line_); 157 } 158 159 void OmniboxPopupModel::TryDeletingCurrentItem() { 160 // We could use GetInfoForCurrentText() here, but it seems better to try 161 // and shift-delete the actual selection, rather than any "in progress, not 162 // yet visible" one. 163 if (selected_line_ == kNoMatch) 164 return; 165 166 // Cancel the query so the matches don't change on the user. 167 autocomplete_controller()->Stop(false); 168 169 const AutocompleteMatch& match = result().match_at(selected_line_); 170 if (match.deletable) { 171 const size_t selected_line = selected_line_; 172 const bool was_temporary_text = !manually_selected_match_.empty(); 173 174 // This will synchronously notify both the edit and us that the results 175 // have changed, causing both to revert to the default match. 176 autocomplete_controller()->DeleteMatch(match); 177 const AutocompleteResult& result = this->result(); 178 if (!result.empty() && 179 (was_temporary_text || selected_line != selected_line_)) { 180 // Move the selection to the next choice after the deleted one. 181 // SetSelectedLine() will clamp to take care of the case where we deleted 182 // the last item. 183 // TODO(pkasting): Eventually the controller should take care of this 184 // before notifying us, reducing flicker. At that point the check for 185 // deletability can move there too. 186 SetSelectedLine(selected_line, false, true); 187 } 188 } 189 } 190 191 gfx::Image OmniboxPopupModel::GetIconIfExtensionMatch( 192 const AutocompleteMatch& match) const { 193 Profile* profile = edit_model_->profile(); 194 const TemplateURL* template_url = match.GetTemplateURL(profile, false); 195 if (template_url && 196 (template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION)) { 197 return extensions::OmniboxAPI::Get(profile)->GetOmniboxPopupIcon( 198 template_url->GetExtensionId()); 199 } 200 return gfx::Image(); 201 } 202 203 void OmniboxPopupModel::OnResultChanged() { 204 const AutocompleteResult& result = this->result(); 205 selected_line_ = result.default_match() == result.end() ? 206 kNoMatch : static_cast<size_t>(result.default_match() - result.begin()); 207 // There had better not be a nonempty result set with no default match. 208 CHECK((selected_line_ != kNoMatch) || result.empty()); 209 manually_selected_match_.Clear(); 210 selected_line_state_ = NORMAL; 211 // If we're going to trim the window size to no longer include the hovered 212 // line, turn hover off. Practically, this shouldn't happen, but it 213 // doesn't hurt to be defensive. 214 if ((hovered_line_ != kNoMatch) && (result.size() <= hovered_line_)) 215 SetHoveredLine(kNoMatch); 216 217 bool popup_was_open = view_->IsOpen(); 218 view_->UpdatePopupAppearance(); 219 // If popup has just been shown or hidden, notify observers. 220 if (view_->IsOpen() != popup_was_open) { 221 FOR_EACH_OBSERVER(OmniboxPopupModelObserver, observers_, 222 OnOmniboxPopupShownOrHidden()); 223 } 224 } 225 226 void OmniboxPopupModel::AddObserver(OmniboxPopupModelObserver* observer) { 227 observers_.AddObserver(observer); 228 } 229 230 void OmniboxPopupModel::RemoveObserver(OmniboxPopupModelObserver* observer) { 231 observers_.RemoveObserver(observer); 232 } 233