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