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/bookmarks/bookmark_model_factory.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_service_factory.h" 15 #include "chrome/browser/ui/omnibox/omnibox_popup_model_observer.h" 16 #include "chrome/browser/ui/omnibox/omnibox_popup_view.h" 17 #include "components/bookmarks/browser/bookmark_model.h" 18 #include "components/omnibox/autocomplete_match.h" 19 #include "components/search_engines/template_url.h" 20 #include "components/search_engines/template_url_service.h" 21 #include "third_party/icu/source/common/unicode/ubidi.h" 22 #include "ui/gfx/image/image.h" 23 #include "ui/gfx/rect.h" 24 25 /////////////////////////////////////////////////////////////////////////////// 26 // OmniboxPopupModel 27 28 const size_t OmniboxPopupModel::kNoMatch = static_cast<size_t>(-1); 29 30 OmniboxPopupModel::OmniboxPopupModel( 31 OmniboxPopupView* popup_view, 32 OmniboxEditModel* edit_model) 33 : view_(popup_view), 34 edit_model_(edit_model), 35 hovered_line_(kNoMatch), 36 selected_line_(kNoMatch), 37 selected_line_state_(NORMAL) { 38 edit_model->set_popup_model(this); 39 } 40 41 OmniboxPopupModel::~OmniboxPopupModel() { 42 } 43 44 // static 45 void OmniboxPopupModel::ComputeMatchMaxWidths(int contents_width, 46 int separator_width, 47 int description_width, 48 int available_width, 49 bool allow_shrinking_contents, 50 int* contents_max_width, 51 int* description_max_width) { 52 if (available_width <= 0) { 53 *contents_max_width = 0; 54 *description_max_width = 0; 55 return; 56 } 57 58 *contents_max_width = contents_width; 59 *description_max_width = description_width; 60 61 // If the description is empty, the contents can get the full width. 62 if (!description_width) 63 return; 64 65 available_width -= separator_width; 66 67 if (contents_width + description_width > available_width) { 68 if (allow_shrinking_contents) { 69 // Try to split the available space fairly between contents and 70 // description (if one wants less than half, give it all it wants and 71 // give the other the remaining space; otherwise, give each half). 72 // However, if this makes the contents too narrow to show a significant 73 // amount of information, give the contents more space. 74 *contents_max_width = std::max( 75 (available_width + 1) / 2, available_width - description_width); 76 77 const int kMinimumContentsWidth = 300; 78 *contents_max_width = std::min( 79 std::max(*contents_max_width, kMinimumContentsWidth), contents_width); 80 } 81 82 // Give the description the remaining space, unless this makes it too small 83 // to display anything meaningful, in which case just hide the description 84 // and let the contents take up the whole width. 85 *description_max_width = available_width - *contents_max_width; 86 const int kMinimumDescriptionWidth = 75; 87 if (*description_max_width < 88 std::min(description_width, kMinimumDescriptionWidth)) { 89 *description_max_width = 0; 90 *contents_max_width = contents_width; 91 } 92 } 93 } 94 95 bool OmniboxPopupModel::IsOpen() const { 96 return view_->IsOpen(); 97 } 98 99 void OmniboxPopupModel::SetHoveredLine(size_t line) { 100 const bool is_disabling = (line == kNoMatch); 101 DCHECK(is_disabling || (line < result().size())); 102 103 if (line == hovered_line_) 104 return; // Nothing to do 105 106 // Make sure the old hovered line is redrawn. No need to redraw the selected 107 // line since selection overrides hover so the appearance won't change. 108 if ((hovered_line_ != kNoMatch) && (hovered_line_ != selected_line_)) 109 view_->InvalidateLine(hovered_line_); 110 111 // Change the hover to the new line. 112 hovered_line_ = line; 113 if (!is_disabling && (hovered_line_ != selected_line_)) 114 view_->InvalidateLine(hovered_line_); 115 } 116 117 void OmniboxPopupModel::SetSelectedLine(size_t line, 118 bool reset_to_default, 119 bool force) { 120 const AutocompleteResult& result = this->result(); 121 if (result.empty()) 122 return; 123 124 // Cancel the query so the matches don't change on the user. 125 autocomplete_controller()->Stop(false); 126 127 line = std::min(line, result.size() - 1); 128 const AutocompleteMatch& match = result.match_at(line); 129 if (reset_to_default) { 130 manually_selected_match_.Clear(); 131 } else { 132 // Track the user's selection until they cancel it. 133 manually_selected_match_.destination_url = match.destination_url; 134 manually_selected_match_.provider_affinity = match.provider; 135 manually_selected_match_.is_history_what_you_typed_match = 136 match.is_history_what_you_typed_match; 137 } 138 139 if (line == selected_line_ && !force) 140 return; // Nothing else to do. 141 142 // We need to update |selected_line_state_| and |selected_line_| before 143 // calling InvalidateLine(), since it will check them to determine how to 144 // draw. We also need to update |selected_line_| before calling 145 // OnPopupDataChanged(), so that when the edit notifies its controller that 146 // something has changed, the controller can get the correct updated data. 147 // 148 // NOTE: We should never reach here with no selected line; the same code that 149 // opened the popup and made it possible to get here should have also set a 150 // selected line. 151 CHECK(selected_line_ != kNoMatch); 152 GURL current_destination(result.match_at(selected_line_).destination_url); 153 const size_t prev_selected_line = selected_line_; 154 selected_line_state_ = NORMAL; 155 selected_line_ = line; 156 view_->InvalidateLine(prev_selected_line); 157 view_->InvalidateLine(selected_line_); 158 159 // Update the edit with the new data for this match. 160 // TODO(pkasting): If |selected_line_| moves to the controller, this can be 161 // eliminated and just become a call to the observer on the edit. 162 base::string16 keyword; 163 bool is_keyword_hint; 164 TemplateURLService* service = 165 TemplateURLServiceFactory::GetForProfile(edit_model_->profile()); 166 match.GetKeywordUIState(service, &keyword, &is_keyword_hint); 167 168 if (reset_to_default) { 169 edit_model_->OnPopupDataChanged(match.inline_autocompletion, NULL, 170 keyword, is_keyword_hint); 171 } else { 172 edit_model_->OnPopupDataChanged(match.fill_into_edit, ¤t_destination, 173 keyword, is_keyword_hint); 174 } 175 176 // Repaint old and new selected lines immediately, so that the edit doesn't 177 // appear to update [much] faster than the popup. 178 view_->PaintUpdatesNow(); 179 } 180 181 void OmniboxPopupModel::ResetToDefaultMatch() { 182 const AutocompleteResult& result = this->result(); 183 CHECK(!result.empty()); 184 SetSelectedLine(result.default_match() - result.begin(), true, false); 185 view_->OnDragCanceled(); 186 } 187 188 void OmniboxPopupModel::Move(int count) { 189 const AutocompleteResult& result = this->result(); 190 if (result.empty()) 191 return; 192 193 // The user is using the keyboard to change the selection, so stop tracking 194 // hover. 195 SetHoveredLine(kNoMatch); 196 197 // Clamp the new line to [0, result_.count() - 1]. 198 const size_t new_line = selected_line_ + count; 199 SetSelectedLine(((count < 0) && (new_line >= selected_line_)) ? 0 : new_line, 200 false, false); 201 } 202 203 void OmniboxPopupModel::SetSelectedLineState(LineState state) { 204 DCHECK(!result().empty()); 205 DCHECK_NE(kNoMatch, selected_line_); 206 207 const AutocompleteMatch& match = result().match_at(selected_line_); 208 DCHECK(match.associated_keyword.get()); 209 210 selected_line_state_ = state; 211 view_->InvalidateLine(selected_line_); 212 } 213 214 void OmniboxPopupModel::TryDeletingCurrentItem() { 215 // We could use GetInfoForCurrentText() here, but it seems better to try 216 // and shift-delete the actual selection, rather than any "in progress, not 217 // yet visible" one. 218 if (selected_line_ == kNoMatch) 219 return; 220 221 // Cancel the query so the matches don't change on the user. 222 autocomplete_controller()->Stop(false); 223 224 const AutocompleteMatch& match = result().match_at(selected_line_); 225 if (match.SupportsDeletion()) { 226 const size_t selected_line = selected_line_; 227 const bool was_temporary_text = !manually_selected_match_.empty(); 228 229 // This will synchronously notify both the edit and us that the results 230 // have changed, causing both to revert to the default match. 231 autocomplete_controller()->DeleteMatch(match); 232 const AutocompleteResult& result = this->result(); 233 if (!result.empty() && 234 (was_temporary_text || selected_line != selected_line_)) { 235 // Move the selection to the next choice after the deleted one. 236 // SetSelectedLine() will clamp to take care of the case where we deleted 237 // the last item. 238 // TODO(pkasting): Eventually the controller should take care of this 239 // before notifying us, reducing flicker. At that point the check for 240 // deletability can move there too. 241 SetSelectedLine(selected_line, false, true); 242 } 243 } 244 } 245 246 gfx::Image OmniboxPopupModel::GetIconIfExtensionMatch( 247 const AutocompleteMatch& match) const { 248 Profile* profile = edit_model_->profile(); 249 TemplateURLService* service = 250 TemplateURLServiceFactory::GetForProfile(profile); 251 const TemplateURL* template_url = match.GetTemplateURL(service, false); 252 if (template_url && 253 (template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION)) { 254 return extensions::OmniboxAPI::Get(profile)->GetOmniboxPopupIcon( 255 template_url->GetExtensionId()); 256 } 257 return gfx::Image(); 258 } 259 260 bool OmniboxPopupModel::IsStarredMatch(const AutocompleteMatch& match) const { 261 Profile* profile = edit_model_->profile(); 262 BookmarkModel* bookmark_model = BookmarkModelFactory::GetForProfile(profile); 263 return bookmark_model && bookmark_model->IsBookmarked(match.destination_url); 264 } 265 266 void OmniboxPopupModel::OnResultChanged() { 267 const AutocompleteResult& result = this->result(); 268 selected_line_ = result.default_match() == result.end() ? 269 kNoMatch : static_cast<size_t>(result.default_match() - result.begin()); 270 // There had better not be a nonempty result set with no default match. 271 CHECK((selected_line_ != kNoMatch) || result.empty()); 272 manually_selected_match_.Clear(); 273 selected_line_state_ = NORMAL; 274 // If we're going to trim the window size to no longer include the hovered 275 // line, turn hover off. Practically, this shouldn't happen, but it 276 // doesn't hurt to be defensive. 277 if ((hovered_line_ != kNoMatch) && (result.size() <= hovered_line_)) 278 SetHoveredLine(kNoMatch); 279 280 bool popup_was_open = view_->IsOpen(); 281 view_->UpdatePopupAppearance(); 282 // If popup has just been shown or hidden, notify observers. 283 if (view_->IsOpen() != popup_was_open) { 284 FOR_EACH_OBSERVER(OmniboxPopupModelObserver, observers_, 285 OnOmniboxPopupShownOrHidden()); 286 } 287 } 288 289 void OmniboxPopupModel::AddObserver(OmniboxPopupModelObserver* observer) { 290 observers_.AddObserver(observer); 291 } 292 293 void OmniboxPopupModel::RemoveObserver(OmniboxPopupModelObserver* observer) { 294 observers_.RemoveObserver(observer); 295 } 296