• 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 
42 // static
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)43 void OmniboxPopupModel::ComputeMatchMaxWidths(int contents_width,
44                                               int separator_width,
45                                               int description_width,
46                                               int available_width,
47                                               bool allow_shrinking_contents,
48                                               int* contents_max_width,
49                                               int* description_max_width) {
50   if (available_width <= 0) {
51     *contents_max_width = 0;
52     *description_max_width = 0;
53     return;
54   }
55 
56   *contents_max_width = contents_width;
57   *description_max_width = description_width;
58 
59   // If the description is empty, the contents can get the full width.
60   if (!description_width)
61     return;
62 
63   available_width -= separator_width;
64 
65   if (contents_width + description_width > available_width) {
66     if (allow_shrinking_contents) {
67       // Try to split the available space fairly between contents and
68       // description (if one wants less than half, give it all it wants and
69       // give the other the remaining space; otherwise, give each half).
70       // However, if this makes the contents too narrow to show a significant
71       // amount of information, give the contents more space.
72       *contents_max_width = std::max(
73           (available_width + 1) / 2, available_width - description_width);
74 
75       const int kMinimumContentsWidth = 300;
76       *contents_max_width = std::min(
77           std::max(*contents_max_width, kMinimumContentsWidth), contents_width);
78     }
79 
80     // Give the description the remaining space, unless this makes it too small
81     // to display anything meaningful, in which case just hide the description
82     // and let the contents take up the whole width.
83     *description_max_width = available_width - *contents_max_width;
84     const int kMinimumDescriptionWidth = 75;
85     if (*description_max_width <
86         std::min(description_width, kMinimumDescriptionWidth)) {
87       *description_max_width = 0;
88       *contents_max_width = contents_width;
89     }
90   }
91 }
92 
IsOpen() const93 bool OmniboxPopupModel::IsOpen() const {
94   return view_->IsOpen();
95 }
96 
SetHoveredLine(size_t line)97 void OmniboxPopupModel::SetHoveredLine(size_t line) {
98   const bool is_disabling = (line == kNoMatch);
99   DCHECK(is_disabling || (line < result().size()));
100 
101   if (line == hovered_line_)
102     return;  // Nothing to do
103 
104   // Make sure the old hovered line is redrawn.  No need to redraw the selected
105   // line since selection overrides hover so the appearance won't change.
106   if ((hovered_line_ != kNoMatch) && (hovered_line_ != selected_line_))
107     view_->InvalidateLine(hovered_line_);
108 
109   // Change the hover to the new line.
110   hovered_line_ = line;
111   if (!is_disabling && (hovered_line_ != selected_line_))
112     view_->InvalidateLine(hovered_line_);
113 }
114 
SetSelectedLine(size_t line,bool reset_to_default,bool force)115 void OmniboxPopupModel::SetSelectedLine(size_t line,
116                                         bool reset_to_default,
117                                         bool force) {
118   const AutocompleteResult& result = this->result();
119   if (result.empty())
120     return;
121 
122   // Cancel the query so the matches don't change on the user.
123   autocomplete_controller()->Stop(false);
124 
125   line = std::min(line, result.size() - 1);
126   const AutocompleteMatch& match = result.match_at(line);
127   if (reset_to_default) {
128     manually_selected_match_.Clear();
129   } else {
130     // Track the user's selection until they cancel it.
131     manually_selected_match_.destination_url = match.destination_url;
132     manually_selected_match_.provider_affinity = match.provider;
133     manually_selected_match_.is_history_what_you_typed_match =
134         match.is_history_what_you_typed_match;
135   }
136 
137   if (line == selected_line_ && !force)
138     return;  // Nothing else to do.
139 
140   // We need to update |selected_line_state_| and |selected_line_| before
141   // calling InvalidateLine(), since it will check them to determine how to
142   // draw.  We also need to update |selected_line_| before calling
143   // OnPopupDataChanged(), so that when the edit notifies its controller that
144   // something has changed, the controller can get the correct updated data.
145   //
146   // NOTE: We should never reach here with no selected line; the same code that
147   // opened the popup and made it possible to get here should have also set a
148   // selected line.
149   CHECK(selected_line_ != kNoMatch);
150   GURL current_destination(result.match_at(selected_line_).destination_url);
151   const size_t prev_selected_line = selected_line_;
152   selected_line_state_ = NORMAL;
153   selected_line_ = line;
154   view_->InvalidateLine(prev_selected_line);
155   view_->InvalidateLine(selected_line_);
156 
157   // Update the edit with the new data for this match.
158   // TODO(pkasting): If |selected_line_| moves to the controller, this can be
159   // eliminated and just become a call to the observer on the edit.
160   base::string16 keyword;
161   bool is_keyword_hint;
162   match.GetKeywordUIState(edit_model_->profile(), &keyword, &is_keyword_hint);
163 
164   if (reset_to_default) {
165     edit_model_->OnPopupDataChanged(match.inline_autocompletion, NULL,
166                                     keyword, is_keyword_hint);
167   } else {
168     edit_model_->OnPopupDataChanged(match.fill_into_edit, &current_destination,
169                                     keyword, is_keyword_hint);
170   }
171 
172   // Repaint old and new selected lines immediately, so that the edit doesn't
173   // appear to update [much] faster than the popup.
174   view_->PaintUpdatesNow();
175 }
176 
ResetToDefaultMatch()177 void OmniboxPopupModel::ResetToDefaultMatch() {
178   const AutocompleteResult& result = this->result();
179   CHECK(!result.empty());
180   SetSelectedLine(result.default_match() - result.begin(), true, false);
181   view_->OnDragCanceled();
182 }
183 
Move(int count)184 void OmniboxPopupModel::Move(int count) {
185   const AutocompleteResult& result = this->result();
186   if (result.empty())
187     return;
188 
189   // The user is using the keyboard to change the selection, so stop tracking
190   // hover.
191   SetHoveredLine(kNoMatch);
192 
193   // Clamp the new line to [0, result_.count() - 1].
194   const size_t new_line = selected_line_ + count;
195   SetSelectedLine(((count < 0) && (new_line >= selected_line_)) ? 0 : new_line,
196                   false, false);
197 }
198 
SetSelectedLineState(LineState state)199 void OmniboxPopupModel::SetSelectedLineState(LineState state) {
200   DCHECK(!result().empty());
201   DCHECK_NE(kNoMatch, selected_line_);
202 
203   const AutocompleteMatch& match = result().match_at(selected_line_);
204   DCHECK(match.associated_keyword.get());
205 
206   selected_line_state_ = state;
207   view_->InvalidateLine(selected_line_);
208 }
209 
TryDeletingCurrentItem()210 void OmniboxPopupModel::TryDeletingCurrentItem() {
211   // We could use GetInfoForCurrentText() here, but it seems better to try
212   // and shift-delete the actual selection, rather than any "in progress, not
213   // yet visible" one.
214   if (selected_line_ == kNoMatch)
215     return;
216 
217   // Cancel the query so the matches don't change on the user.
218   autocomplete_controller()->Stop(false);
219 
220   const AutocompleteMatch& match = result().match_at(selected_line_);
221   if (match.SupportsDeletion()) {
222     const size_t selected_line = selected_line_;
223     const bool was_temporary_text = !manually_selected_match_.empty();
224 
225     // This will synchronously notify both the edit and us that the results
226     // have changed, causing both to revert to the default match.
227     autocomplete_controller()->DeleteMatch(match);
228     const AutocompleteResult& result = this->result();
229     if (!result.empty() &&
230         (was_temporary_text || selected_line != selected_line_)) {
231       // Move the selection to the next choice after the deleted one.
232       // SetSelectedLine() will clamp to take care of the case where we deleted
233       // the last item.
234       // TODO(pkasting): Eventually the controller should take care of this
235       // before notifying us, reducing flicker.  At that point the check for
236       // deletability can move there too.
237       SetSelectedLine(selected_line, false, true);
238     }
239   }
240 }
241 
GetIconIfExtensionMatch(const AutocompleteMatch & match) const242 gfx::Image OmniboxPopupModel::GetIconIfExtensionMatch(
243     const AutocompleteMatch& match) const {
244   Profile* profile = edit_model_->profile();
245   const TemplateURL* template_url = match.GetTemplateURL(profile, false);
246   if (template_url &&
247       (template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION)) {
248     return extensions::OmniboxAPI::Get(profile)->GetOmniboxPopupIcon(
249         template_url->GetExtensionId());
250   }
251   return gfx::Image();
252 }
253 
OnResultChanged()254 void OmniboxPopupModel::OnResultChanged() {
255   const AutocompleteResult& result = this->result();
256   selected_line_ = result.default_match() == result.end() ?
257       kNoMatch : static_cast<size_t>(result.default_match() - result.begin());
258   // There had better not be a nonempty result set with no default match.
259   CHECK((selected_line_ != kNoMatch) || result.empty());
260   manually_selected_match_.Clear();
261   selected_line_state_ = NORMAL;
262   // If we're going to trim the window size to no longer include the hovered
263   // line, turn hover off.  Practically, this shouldn't happen, but it
264   // doesn't hurt to be defensive.
265   if ((hovered_line_ != kNoMatch) && (result.size() <= hovered_line_))
266     SetHoveredLine(kNoMatch);
267 
268   bool popup_was_open = view_->IsOpen();
269   view_->UpdatePopupAppearance();
270   // If popup has just been shown or hidden, notify observers.
271   if (view_->IsOpen() != popup_was_open) {
272     FOR_EACH_OBSERVER(OmniboxPopupModelObserver, observers_,
273                       OnOmniboxPopupShownOrHidden());
274   }
275 }
276 
AddObserver(OmniboxPopupModelObserver * observer)277 void OmniboxPopupModel::AddObserver(OmniboxPopupModelObserver* observer) {
278   observers_.AddObserver(observer);
279 }
280 
RemoveObserver(OmniboxPopupModelObserver * observer)281 void OmniboxPopupModel::RemoveObserver(OmniboxPopupModelObserver* observer) {
282   observers_.RemoveObserver(observer);
283 }
284