// 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 #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_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" #include "ui/gfx/image/image.h" using bookmarks::BookmarkModel; /////////////////////////////////////////////////////////////////////////////// // OmniboxPopupModel const size_t OmniboxPopupModel::kNoMatch = static_cast(-1); OmniboxPopupModel::OmniboxPopupModel( OmniboxPopupView* popup_view, OmniboxEditModel* edit_model) : view_(popup_view), edit_model_(edit_model), hovered_line_(kNoMatch), selected_line_(kNoMatch), selected_line_state_(NORMAL) { 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 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 = description_width; // If the description is empty, the contents can get the full width. if (!description_width) 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::SetHoveredLine(size_t line) { const bool is_disabling = (line == kNoMatch); DCHECK(is_disabling || (line < result().size())); if (line == hovered_line_) return; // Nothing to do // Make sure the old hovered line is redrawn. No need to redraw the selected // line since selection overrides hover so the appearance won't change. if ((hovered_line_ != kNoMatch) && (hovered_line_ != selected_line_)) view_->InvalidateLine(hovered_line_); // Change the hover to the new line. hovered_line_ = line; if (!is_disabling && (hovered_line_ != selected_line_)) view_->InvalidateLine(hovered_line_); } 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); if (reset_to_default) { manually_selected_match_.Clear(); } else { // Track the user's selection until they cancel it. manually_selected_match_.destination_url = match.destination_url; manually_selected_match_.provider_affinity = match.provider; manually_selected_match_.is_history_what_you_typed_match = match.type == AutocompleteMatchType::URL_WHAT_YOU_TYPED; } 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_); // 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, NULL, 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; // The user is using the keyboard to change the selection, so stop tracking // hover. SetHoveredLine(kNoMatch); // 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 = !manually_selected_match_.empty(); // 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); } } } gfx::Image OmniboxPopupModel::GetIconIfExtensionMatch( const AutocompleteMatch& match) const { return edit_model_->client()->GetIconIfExtensionMatch(match); } bool OmniboxPopupModel::IsStarredMatch(const AutocompleteMatch& match) const { BookmarkModel* 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(result.default_match() - result.begin()); // There had better not be a nonempty result set with no default match. CHECK((selected_line_ != kNoMatch) || result.empty()); manually_selected_match_.Clear(); selected_line_state_ = NORMAL; // If we're going to trim the window size to no longer include the hovered // line, turn hover off. Practically, this shouldn't happen, but it // doesn't hurt to be defensive. if ((hovered_line_ != kNoMatch) && (result.size() <= hovered_line_)) SetHoveredLine(kNoMatch); 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_EACH_OBSERVER(OmniboxPopupModelObserver, observers_, 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(); }