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, ¤t_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