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 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 && template_url->IsExtensionKeyword()) { 196 return extensions::OmniboxAPI::Get(profile)->GetOmniboxPopupIcon( 197 template_url->GetExtensionId()); 198 } 199 return gfx::Image(); 200 } 201 202 void OmniboxPopupModel::OnResultChanged() { 203 const AutocompleteResult& result = this->result(); 204 selected_line_ = result.default_match() == result.end() ? 205 kNoMatch : static_cast<size_t>(result.default_match() - result.begin()); 206 // There had better not be a nonempty result set with no default match. 207 CHECK((selected_line_ != kNoMatch) || result.empty()); 208 manually_selected_match_.Clear(); 209 selected_line_state_ = NORMAL; 210 // If we're going to trim the window size to no longer include the hovered 211 // line, turn hover off. Practically, this shouldn't happen, but it 212 // doesn't hurt to be defensive. 213 if ((hovered_line_ != kNoMatch) && (result.size() <= hovered_line_)) 214 SetHoveredLine(kNoMatch); 215 216 bool popup_was_open = view_->IsOpen(); 217 view_->UpdatePopupAppearance(); 218 // If popup has just been shown or hidden, notify observers. 219 if (view_->IsOpen() != popup_was_open) { 220 FOR_EACH_OBSERVER(OmniboxPopupModelObserver, observers_, 221 OnOmniboxPopupShownOrHidden()); 222 } 223 } 224 225 void OmniboxPopupModel::AddObserver(OmniboxPopupModelObserver* observer) { 226 observers_.AddObserver(observer); 227 } 228 229 void OmniboxPopupModel::RemoveObserver(OmniboxPopupModelObserver* observer) { 230 observers_.RemoveObserver(observer); 231 } 232