Home | History | Annotate | Download | only in autocomplete
      1 // Copyright (c) 2011 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/autocomplete/autocomplete_popup_model.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "unicode/ubidi.h"
     10 
     11 #include "base/string_util.h"
     12 #include "base/utf_string_conversions.h"
     13 #include "chrome/browser/autocomplete/autocomplete_edit.h"
     14 #include "chrome/browser/autocomplete/autocomplete_match.h"
     15 #include "chrome/browser/autocomplete/autocomplete_popup_view.h"
     16 #include "chrome/browser/extensions/extension_service.h"
     17 #include "chrome/browser/profiles/profile.h"
     18 #include "chrome/browser/search_engines/template_url.h"
     19 #include "chrome/browser/search_engines/template_url_model.h"
     20 #include "ui/gfx/rect.h"
     21 
     22 ///////////////////////////////////////////////////////////////////////////////
     23 // AutocompletePopupModel
     24 
     25 AutocompletePopupModel::AutocompletePopupModel(
     26     AutocompletePopupView* popup_view,
     27     AutocompleteEditModel* edit_model,
     28     Profile* profile)
     29     : view_(popup_view),
     30       edit_model_(edit_model),
     31       profile_(profile),
     32       hovered_line_(kNoMatch),
     33       selected_line_(kNoMatch) {
     34   edit_model->set_popup_model(this);
     35 }
     36 
     37 AutocompletePopupModel::~AutocompletePopupModel() {
     38 }
     39 
     40 bool AutocompletePopupModel::IsOpen() const {
     41   return view_->IsOpen();
     42 }
     43 
     44 void AutocompletePopupModel::SetHoveredLine(size_t line) {
     45   const bool is_disabling = (line == kNoMatch);
     46   DCHECK(is_disabling || (line < result().size()));
     47 
     48   if (line == hovered_line_)
     49     return;  // Nothing to do
     50 
     51   // Make sure the old hovered line is redrawn.  No need to redraw the selected
     52   // line since selection overrides hover so the appearance won't change.
     53   if ((hovered_line_ != kNoMatch) && (hovered_line_ != selected_line_))
     54     view_->InvalidateLine(hovered_line_);
     55 
     56   // Change the hover to the new line.
     57   hovered_line_ = line;
     58   if (!is_disabling && (hovered_line_ != selected_line_))
     59     view_->InvalidateLine(hovered_line_);
     60 }
     61 
     62 void AutocompletePopupModel::SetSelectedLine(size_t line,
     63                                              bool reset_to_default,
     64                                              bool force) {
     65   const AutocompleteResult& result = this->result();
     66   if (result.empty())
     67     return;
     68 
     69   // Cancel the query so the matches don't change on the user.
     70   autocomplete_controller()->Stop(false);
     71 
     72   line = std::min(line, result.size() - 1);
     73   const AutocompleteMatch& match = result.match_at(line);
     74   if (reset_to_default) {
     75     manually_selected_match_.Clear();
     76   } else {
     77     // Track the user's selection until they cancel it.
     78     manually_selected_match_.destination_url = match.destination_url;
     79     manually_selected_match_.provider_affinity = match.provider;
     80     manually_selected_match_.is_history_what_you_typed_match =
     81         match.is_history_what_you_typed_match;
     82   }
     83 
     84   if (line == selected_line_ && !force)
     85     return;  // Nothing else to do.
     86 
     87   // We need to update |selected_line_| before calling OnPopupDataChanged(), so
     88   // that when the edit notifies its controller that something has changed, the
     89   // controller can get the correct updated data.
     90   //
     91   // NOTE: We should never reach here with no selected line; the same code that
     92   // opened the popup and made it possible to get here should have also set a
     93   // selected line.
     94   CHECK(selected_line_ != kNoMatch);
     95   GURL current_destination(result.match_at(selected_line_).destination_url);
     96   view_->InvalidateLine(selected_line_);
     97   selected_line_ = line;
     98   view_->InvalidateLine(selected_line_);
     99 
    100   // Update the edit with the new data for this match.
    101   // TODO(pkasting): If |selected_line_| moves to the controller, this can be
    102   // eliminated and just become a call to the observer on the edit.
    103   string16 keyword;
    104   const bool is_keyword_hint = GetKeywordForMatch(match, &keyword);
    105   if (reset_to_default) {
    106     string16 inline_autocomplete_text;
    107     if ((match.inline_autocomplete_offset != string16::npos) &&
    108         (match.inline_autocomplete_offset < match.fill_into_edit.length())) {
    109       inline_autocomplete_text =
    110           match.fill_into_edit.substr(match.inline_autocomplete_offset);
    111     }
    112     edit_model_->OnPopupDataChanged(inline_autocomplete_text, NULL,
    113                                     keyword, is_keyword_hint);
    114   } else {
    115     edit_model_->OnPopupDataChanged(match.fill_into_edit, &current_destination,
    116                                     keyword, is_keyword_hint);
    117   }
    118 
    119   // Repaint old and new selected lines immediately, so that the edit doesn't
    120   // appear to update [much] faster than the popup.
    121   view_->PaintUpdatesNow();
    122 }
    123 
    124 void AutocompletePopupModel::ResetToDefaultMatch() {
    125   const AutocompleteResult& result = this->result();
    126   CHECK(!result.empty());
    127   SetSelectedLine(result.default_match() - result.begin(), true, false);
    128   view_->OnDragCanceled();
    129 }
    130 
    131 bool AutocompletePopupModel::GetKeywordForMatch(const AutocompleteMatch& match,
    132                                                 string16* keyword) const {
    133   // If the current match is a keyword, return that as the selected keyword.
    134   if (TemplateURL::SupportsReplacement(match.template_url)) {
    135     keyword->assign(match.template_url->keyword());
    136     return false;
    137   }
    138 
    139   // See if the current match's fill_into_edit corresponds to a keyword.
    140   return GetKeywordForText(match.fill_into_edit, keyword);
    141 }
    142 
    143 bool AutocompletePopupModel::GetKeywordForText(const string16& text,
    144                                                string16* keyword) const {
    145   // Creates keyword_hint first in case |keyword| is a pointer to |text|.
    146   const string16 keyword_hint(TemplateURLModel::CleanUserInputKeyword(text));
    147 
    148   // Assume we have no keyword until we find otherwise.
    149   keyword->clear();
    150 
    151   if (keyword_hint.empty())
    152     return false;
    153   if (!profile_->GetTemplateURLModel())
    154     return false;
    155   profile_->GetTemplateURLModel()->Load();
    156 
    157   // Don't provide a hint if this keyword doesn't support replacement.
    158   const TemplateURL* const template_url =
    159       profile_->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword_hint);
    160   if (!TemplateURL::SupportsReplacement(template_url))
    161     return false;
    162 
    163   // Don't provide a hint for inactive/disabled extension keywords.
    164   if (template_url->IsExtensionKeyword()) {
    165     const Extension* extension = profile_->GetExtensionService()->
    166         GetExtensionById(template_url->GetExtensionId(), false);
    167     if (!extension ||
    168         (profile_->IsOffTheRecord() &&
    169          !profile_->GetExtensionService()->
    170              IsIncognitoEnabled(extension->id())))
    171       return false;
    172   }
    173 
    174   keyword->assign(keyword_hint);
    175   return true;
    176 }
    177 
    178 void AutocompletePopupModel::Move(int count) {
    179   const AutocompleteResult& result = this->result();
    180   if (result.empty())
    181     return;
    182 
    183   // The user is using the keyboard to change the selection, so stop tracking
    184   // hover.
    185   SetHoveredLine(kNoMatch);
    186 
    187   // Clamp the new line to [0, result_.count() - 1].
    188   const size_t new_line = selected_line_ + count;
    189   SetSelectedLine(((count < 0) && (new_line >= selected_line_)) ? 0 : new_line,
    190                   false, false);
    191 }
    192 
    193 void AutocompletePopupModel::TryDeletingCurrentItem() {
    194   // We could use InfoForCurrentSelection() here, but it seems better to try
    195   // and shift-delete the actual selection, rather than any "in progress, not
    196   // yet visible" one.
    197   if (selected_line_ == kNoMatch)
    198     return;
    199 
    200   // Cancel the query so the matches don't change on the user.
    201   autocomplete_controller()->Stop(false);
    202 
    203   const AutocompleteMatch& match = result().match_at(selected_line_);
    204   if (match.deletable) {
    205     const size_t selected_line = selected_line_;
    206     const bool was_temporary_text = !manually_selected_match_.empty();
    207 
    208     // This will synchronously notify both the edit and us that the results
    209     // have changed, causing both to revert to the default match.
    210     autocomplete_controller()->DeleteMatch(match);
    211     const AutocompleteResult& result = this->result();
    212     if (!result.empty() &&
    213         (was_temporary_text || selected_line != selected_line_)) {
    214       // Move the selection to the next choice after the deleted one.
    215       // SetSelectedLine() will clamp to take care of the case where we deleted
    216       // the last item.
    217       // TODO(pkasting): Eventually the controller should take care of this
    218       // before notifying us, reducing flicker.  At that point the check for
    219       // deletability can move there too.
    220       SetSelectedLine(selected_line, false, true);
    221     }
    222   }
    223 }
    224 
    225 const SkBitmap* AutocompletePopupModel::GetIconIfExtensionMatch(
    226     const AutocompleteMatch& match) const {
    227   if (!match.template_url || !match.template_url->IsExtensionKeyword())
    228     return NULL;
    229 
    230   return &profile_->GetExtensionService()->GetOmniboxPopupIcon(
    231       match.template_url->GetExtensionId());
    232 }
    233 
    234 void AutocompletePopupModel::OnResultChanged() {
    235   const AutocompleteResult& result = this->result();
    236   selected_line_ = result.default_match() == result.end() ?
    237       kNoMatch : static_cast<size_t>(result.default_match() - result.begin());
    238   // There had better not be a nonempty result set with no default match.
    239   CHECK((selected_line_ != kNoMatch) || result.empty());
    240   manually_selected_match_.Clear();
    241   // If we're going to trim the window size to no longer include the hovered
    242   // line, turn hover off.  Practically, this shouldn't happen, but it
    243   // doesn't hurt to be defensive.
    244   if ((hovered_line_ != kNoMatch) && (result.size() <= hovered_line_))
    245     SetHoveredLine(kNoMatch);
    246 
    247   view_->UpdatePopupAppearance();
    248 }
    249