• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
OmniboxPopupModel(OmniboxPopupView * popup_view,OmniboxEditModel * edit_model)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 
~OmniboxPopupModel()39 OmniboxPopupModel::~OmniboxPopupModel() {
40 }
41 
IsOpen() const42 bool OmniboxPopupModel::IsOpen() const {
43   return view_->IsOpen();
44 }
45 
SetHoveredLine(size_t line)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 
SetSelectedLine(size_t line,bool reset_to_default,bool force)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 
ResetToDefaultMatch()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 
Move(int count)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 
SetSelectedLineState(LineState state)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 
TryDeletingCurrentItem()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 
GetIconIfExtensionMatch(const AutocompleteMatch & match) const191 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 
OnResultChanged()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 
AddObserver(OmniboxPopupModelObserver * observer)226 void OmniboxPopupModel::AddObserver(OmniboxPopupModelObserver* observer) {
227   observers_.AddObserver(observer);
228 }
229 
RemoveObserver(OmniboxPopupModelObserver * observer)230 void OmniboxPopupModel::RemoveObserver(OmniboxPopupModelObserver* observer) {
231   observers_.RemoveObserver(observer);
232 }
233