Home | History | Annotate | Download | only in omnibox
      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, &current_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