| // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | #include "components/omnibox/browser/omnibox_popup_model.h" | 
 |  | 
 | #include <algorithm> | 
 |  | 
 | #include "base/feature_list.h" | 
 | #include "base/strings/string_util.h" | 
 | #include "base/strings/utf_string_conversions.h" | 
 | #include "components/bookmarks/browser/bookmark_model.h" | 
 | #include "components/omnibox/browser/autocomplete_match.h" | 
 | #include "components/omnibox/browser/omnibox_client.h" | 
 | #include "components/omnibox/browser/omnibox_field_trial.h" | 
 | #include "components/omnibox/browser/omnibox_popup_model_observer.h" | 
 | #include "components/omnibox/browser/omnibox_popup_view.h" | 
 | #include "components/search_engines/template_url.h" | 
 | #include "components/search_engines/template_url_service.h" | 
 | #include "third_party/icu/source/common/unicode/ubidi.h" | 
 | #include "ui/gfx/geometry/rect.h" | 
 |  | 
 | #if !defined(OS_ANDROID) && !defined(OS_IOS) | 
 | #include "components/omnibox/browser/vector_icons.h"  // nogncheck | 
 | #include "ui/gfx/paint_vector_icon.h" | 
 | #include "ui/gfx/vector_icon_types.h" | 
 | #endif | 
 |  | 
 | namespace { | 
 |  | 
 | size_t GetFaviconCacheSize() { | 
 |   // Set cache size to twice the number of maximum results to avoid favicon | 
 |   // refetches as the user types. Favicon fetches are uncached and can hit disk. | 
 |   return 2 * AutocompleteResult::GetMaxMatches(); | 
 | } | 
 |  | 
 | }  // namespace | 
 |  | 
 | /////////////////////////////////////////////////////////////////////////////// | 
 | // OmniboxPopupModel | 
 |  | 
 | const size_t OmniboxPopupModel::kNoMatch = static_cast<size_t>(-1); | 
 |  | 
 | OmniboxPopupModel::OmniboxPopupModel(OmniboxPopupView* popup_view, | 
 |                                      OmniboxEditModel* edit_model) | 
 |     : favicons_cache_(GetFaviconCacheSize()), | 
 |       view_(popup_view), | 
 |       edit_model_(edit_model), | 
 |       selected_line_(kNoMatch), | 
 |       selected_line_state_(NORMAL), | 
 |       has_selected_match_(false), | 
 |       weak_factory_(this) { | 
 |   edit_model->set_popup_model(this); | 
 | } | 
 |  | 
 | OmniboxPopupModel::~OmniboxPopupModel() { | 
 | } | 
 |  | 
 | // static | 
 | void OmniboxPopupModel::ComputeMatchMaxWidths(int contents_width, | 
 |                                               int separator_width, | 
 |                                               int description_width, | 
 |                                               int available_width, | 
 |                                               bool description_on_separate_line, | 
 |                                               bool allow_shrinking_contents, | 
 |                                               int* contents_max_width, | 
 |                                               int* description_max_width) { | 
 |   available_width = std::max(available_width, 0); | 
 |   *contents_max_width = std::min(contents_width, available_width); | 
 |   *description_max_width = std::min(description_width, available_width); | 
 |  | 
 |   // If the description is empty, or the contents and description are on | 
 |   // separate lines, each can get the full available width. | 
 |   if (!description_width || description_on_separate_line) | 
 |     return; | 
 |  | 
 |   // If we want to display the description, we need to reserve enough space for | 
 |   // the separator. | 
 |   available_width -= separator_width; | 
 |   if (available_width < 0) { | 
 |     *description_max_width = 0; | 
 |     return; | 
 |   } | 
 |  | 
 |   if (contents_width + description_width > available_width) { | 
 |     if (allow_shrinking_contents) { | 
 |       // Try to split the available space fairly between contents and | 
 |       // description (if one wants less than half, give it all it wants and | 
 |       // give the other the remaining space; otherwise, give each half). | 
 |       // However, if this makes the contents too narrow to show a significant | 
 |       // amount of information, give the contents more space. | 
 |       *contents_max_width = std::max( | 
 |           (available_width + 1) / 2, available_width - description_width); | 
 |  | 
 |       const int kMinimumContentsWidth = 300; | 
 |       *contents_max_width = std::min( | 
 |           std::min(std::max(*contents_max_width, kMinimumContentsWidth), | 
 |                    contents_width), | 
 |           available_width); | 
 |     } | 
 |  | 
 |     // Give the description the remaining space, unless this makes it too small | 
 |     // to display anything meaningful, in which case just hide the description | 
 |     // and let the contents take up the whole width. | 
 |     *description_max_width = | 
 |         std::min(description_width, available_width - *contents_max_width); | 
 |     const int kMinimumDescriptionWidth = 75; | 
 |     if (*description_max_width < | 
 |         std::min(description_width, kMinimumDescriptionWidth)) { | 
 |       *description_max_width = 0; | 
 |       // Since we're not going to display the description, the contents can have | 
 |       // the space we reserved for the separator. | 
 |       available_width += separator_width; | 
 |       *contents_max_width = std::min(contents_width, available_width); | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | bool OmniboxPopupModel::IsOpen() const { | 
 |   return view_->IsOpen(); | 
 | } | 
 |  | 
 | void OmniboxPopupModel::SetSelectedLine(size_t line, | 
 |                                         bool reset_to_default, | 
 |                                         bool force) { | 
 |   const AutocompleteResult& result = this->result(); | 
 |   if (result.empty()) | 
 |     return; | 
 |  | 
 |   // Cancel the query so the matches don't change on the user. | 
 |   autocomplete_controller()->Stop(false); | 
 |  | 
 |   line = std::min(line, result.size() - 1); | 
 |   const AutocompleteMatch& match = result.match_at(line); | 
 |   has_selected_match_ = !reset_to_default; | 
 |  | 
 |   if (line == selected_line_ && !force) | 
 |     return;  // Nothing else to do. | 
 |  | 
 |   // We need to update |selected_line_state_| and |selected_line_| before | 
 |   // calling InvalidateLine(), since it will check them to determine how to | 
 |   // draw.  We also need to update |selected_line_| before calling | 
 |   // OnPopupDataChanged(), so that when the edit notifies its controller that | 
 |   // something has changed, the controller can get the correct updated data. | 
 |   // | 
 |   // NOTE: We should never reach here with no selected line; the same code that | 
 |   // opened the popup and made it possible to get here should have also set a | 
 |   // selected line. | 
 |   CHECK(selected_line_ != kNoMatch); | 
 |   GURL current_destination(result.match_at(selected_line_).destination_url); | 
 |   const size_t prev_selected_line = selected_line_; | 
 |   selected_line_state_ = NORMAL; | 
 |   selected_line_ = line; | 
 |   view_->InvalidateLine(prev_selected_line); | 
 |   view_->InvalidateLine(selected_line_); | 
 |  | 
 |   view_->OnLineSelected(selected_line_); | 
 |  | 
 |   // Update the edit with the new data for this match. | 
 |   // TODO(pkasting): If |selected_line_| moves to the controller, this can be | 
 |   // eliminated and just become a call to the observer on the edit. | 
 |   base::string16 keyword; | 
 |   bool is_keyword_hint; | 
 |   TemplateURLService* service = edit_model_->client()->GetTemplateURLService(); | 
 |   match.GetKeywordUIState(service, &keyword, &is_keyword_hint); | 
 |  | 
 |   if (reset_to_default) { | 
 |     edit_model_->OnPopupDataChanged(match.inline_autocompletion, nullptr, | 
 |                                     keyword, is_keyword_hint); | 
 |   } else { | 
 |     edit_model_->OnPopupDataChanged(match.fill_into_edit, ¤t_destination, | 
 |                                     keyword, is_keyword_hint); | 
 |   } | 
 |  | 
 |   // Repaint old and new selected lines immediately, so that the edit doesn't | 
 |   // appear to update [much] faster than the popup. | 
 |   view_->PaintUpdatesNow(); | 
 | } | 
 |  | 
 | void OmniboxPopupModel::ResetToDefaultMatch() { | 
 |   const AutocompleteResult& result = this->result(); | 
 |   CHECK(!result.empty()); | 
 |   SetSelectedLine(result.default_match() - result.begin(), true, false); | 
 |   view_->OnDragCanceled(); | 
 | } | 
 |  | 
 | void OmniboxPopupModel::Move(int count) { | 
 |   const AutocompleteResult& result = this->result(); | 
 |   if (result.empty()) | 
 |     return; | 
 |  | 
 |   // Clamp the new line to [0, result_.count() - 1]. | 
 |   const size_t new_line = selected_line_ + count; | 
 |   SetSelectedLine(((count < 0) && (new_line >= selected_line_)) ? 0 : new_line, | 
 |                   false, false); | 
 | } | 
 |  | 
 | void OmniboxPopupModel::SetSelectedLineState(LineState state) { | 
 |   DCHECK(!result().empty()); | 
 |   DCHECK_NE(kNoMatch, selected_line_); | 
 |  | 
 |   const AutocompleteMatch& match = result().match_at(selected_line_); | 
 |   DCHECK(match.associated_keyword.get()); | 
 |  | 
 |   selected_line_state_ = state; | 
 |   view_->InvalidateLine(selected_line_); | 
 | } | 
 |  | 
 | void OmniboxPopupModel::TryDeletingCurrentItem() { | 
 |   // We could use GetInfoForCurrentText() here, but it seems better to try | 
 |   // and shift-delete the actual selection, rather than any "in progress, not | 
 |   // yet visible" one. | 
 |   if (selected_line_ == kNoMatch) | 
 |     return; | 
 |  | 
 |   // Cancel the query so the matches don't change on the user. | 
 |   autocomplete_controller()->Stop(false); | 
 |  | 
 |   const AutocompleteMatch& match = result().match_at(selected_line_); | 
 |   if (match.SupportsDeletion()) { | 
 |     const size_t selected_line = selected_line_; | 
 |     const bool was_temporary_text = has_selected_match_; | 
 |  | 
 |     // This will synchronously notify both the edit and us that the results | 
 |     // have changed, causing both to revert to the default match. | 
 |     autocomplete_controller()->DeleteMatch(match); | 
 |     const AutocompleteResult& result = this->result(); | 
 |     if (!result.empty() && | 
 |         (was_temporary_text || selected_line != selected_line_)) { | 
 |       // Move the selection to the next choice after the deleted one. | 
 |       // SetSelectedLine() will clamp to take care of the case where we deleted | 
 |       // the last item. | 
 |       // TODO(pkasting): Eventually the controller should take care of this | 
 |       // before notifying us, reducing flicker.  At that point the check for | 
 |       // deletability can move there too. | 
 |       SetSelectedLine(selected_line, false, true); | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | bool OmniboxPopupModel::IsStarredMatch(const AutocompleteMatch& match) const { | 
 |   auto* bookmark_model = edit_model_->client()->GetBookmarkModel(); | 
 |   return bookmark_model && bookmark_model->IsBookmarked(match.destination_url); | 
 | } | 
 |  | 
 | void OmniboxPopupModel::OnResultChanged() { | 
 |   answer_bitmap_ = SkBitmap(); | 
 |   const AutocompleteResult& result = this->result(); | 
 |   selected_line_ = result.default_match() == result.end() ? | 
 |       kNoMatch : static_cast<size_t>(result.default_match() - result.begin()); | 
 |   // There had better not be a nonempty result set with no default match. | 
 |   CHECK((selected_line_ != kNoMatch) || result.empty()); | 
 |   has_selected_match_ = false; | 
 |   selected_line_state_ = NORMAL; | 
 |  | 
 |   bool popup_was_open = view_->IsOpen(); | 
 |   view_->UpdatePopupAppearance(); | 
 |   // If popup has just been shown or hidden, notify observers. | 
 |   if (view_->IsOpen() != popup_was_open) { | 
 |     for (OmniboxPopupModelObserver& observer : observers_) | 
 |       observer.OnOmniboxPopupShownOrHidden(); | 
 |   } | 
 | } | 
 |  | 
 | void OmniboxPopupModel::AddObserver(OmniboxPopupModelObserver* observer) { | 
 |   observers_.AddObserver(observer); | 
 | } | 
 |  | 
 | void OmniboxPopupModel::RemoveObserver(OmniboxPopupModelObserver* observer) { | 
 |   observers_.RemoveObserver(observer); | 
 | } | 
 |  | 
 | void OmniboxPopupModel::SetAnswerBitmap(const SkBitmap& bitmap) { | 
 |   answer_bitmap_ = bitmap; | 
 |   view_->UpdatePopupAppearance(); | 
 | } | 
 |  | 
 | // Android and iOS have their own platform-specific icon logic. | 
 | #if !defined(OS_ANDROID) && !defined(OS_IOS) | 
 | gfx::Image OmniboxPopupModel::GetMatchIcon(const AutocompleteMatch& match, | 
 |                                            SkColor vector_icon_color) { | 
 |   gfx::Image extension_icon = | 
 |       edit_model_->client()->GetIconIfExtensionMatch(match); | 
 |   if (!extension_icon.IsEmpty()) | 
 |     return extension_icon; | 
 |  | 
 |   if (base::FeatureList::IsEnabled( | 
 |           omnibox::kUIExperimentShowSuggestionFavicons) && | 
 |       !AutocompleteMatch::IsSearchType(match.type)) { | 
 |     const GURL& page_url = match.destination_url; | 
 |     auto cache_iterator = favicons_cache_.Get(page_url); | 
 |     if (cache_iterator != favicons_cache_.end() && | 
 |         !cache_iterator->second.IsEmpty()) { | 
 |       return cache_iterator->second; | 
 |     } | 
 |  | 
 |     // We don't have the favicon in the cache. We kick off the request, but | 
 |     // don't early return. We proceed to return the vector icon for the match | 
 |     // type. If and when we ever get the favicon back, we send a notification. | 
 |     // | 
 |     // Note: We're relying on GetFaviconForPageUrl to call the callback | 
 |     // asynchronously. If the callback is called synchronously, the fetched | 
 |     // favicon may get clobbered by the vector icon once this method returns. | 
 |     edit_model_->client()->GetFaviconForPageUrl( | 
 |         &favicon_task_tracker_, match.destination_url, | 
 |         base::Bind(&OmniboxPopupModel::OnFaviconFetched, | 
 |                    weak_factory_.GetWeakPtr(), match.destination_url)); | 
 |   } | 
 |  | 
 |   const auto& vector_icon_type = | 
 |       IsStarredMatch(match) ? omnibox::kStarIcon | 
 |                             : AutocompleteMatch::TypeToVectorIcon(match.type); | 
 |   return gfx::Image( | 
 |       gfx::CreateVectorIcon(vector_icon_type, 16, vector_icon_color)); | 
 | } | 
 | #endif  // !defined(OS_ANDROID) && !defined(OS_IOS) | 
 |  | 
 | void OmniboxPopupModel::OnFaviconFetched(const GURL& page_url, | 
 |                                          const gfx::Image& icon) { | 
 |   if (icon.IsEmpty()) | 
 |     return; | 
 |  | 
 |   favicons_cache_.Put(page_url, icon); | 
 |  | 
 |   // Notify all affected matches. | 
 |   for (size_t i = 0; i < result().size(); ++i) { | 
 |     auto& match = result().match_at(i); | 
 |     if (!AutocompleteMatch::IsSearchType(match.type) && | 
 |         match.destination_url == page_url) { | 
 |       view_->OnMatchIconUpdated(i); | 
 |     } | 
 |   } | 
 | } |