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.
5 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
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"
23 ///////////////////////////////////////////////////////////////////////////////
26 const size_t OmniboxPopupModel::kNoMatch
= -1;
28 OmniboxPopupModel::OmniboxPopupModel(
29 OmniboxPopupView
* popup_view
,
30 OmniboxEditModel
* edit_model
)
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);
39 OmniboxPopupModel::~OmniboxPopupModel() {
43 void OmniboxPopupModel::ComputeMatchMaxWidths(int contents_width
,
45 int description_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;
56 *contents_max_width
= contents_width
;
57 *description_max_width
= description_width
;
59 // If the description is empty, the contents can get the full width.
60 if (!description_width
)
63 available_width
-= separator_width
;
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
);
75 const int kMinimumContentsWidth
= 300;
76 *contents_max_width
= std::min(
77 std::max(*contents_max_width
, kMinimumContentsWidth
), contents_width
);
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
;
93 bool OmniboxPopupModel::IsOpen() const {
94 return view_
->IsOpen();
97 void OmniboxPopupModel::SetHoveredLine(size_t line
) {
98 const bool is_disabling
= (line
== kNoMatch
);
99 DCHECK(is_disabling
|| (line
< result().size()));
101 if (line
== hovered_line_
)
102 return; // Nothing to do
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_
);
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_
);
115 void OmniboxPopupModel::SetSelectedLine(size_t line
,
116 bool reset_to_default
,
118 const AutocompleteResult
& result
= this->result();
122 // Cancel the query so the matches don't change on the user.
123 autocomplete_controller()->Stop(false);
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();
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
;
137 if (line
== selected_line_
&& !force
)
138 return; // Nothing else to do.
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.
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
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_
);
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
);
164 if (reset_to_default
) {
165 edit_model_
->OnPopupDataChanged(match
.inline_autocompletion
, NULL
,
166 keyword
, is_keyword_hint
);
168 edit_model_
->OnPopupDataChanged(match
.fill_into_edit
, ¤t_destination
,
169 keyword
, is_keyword_hint
);
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();
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();
184 void OmniboxPopupModel::Move(int count
) {
185 const AutocompleteResult
& result
= this->result();
189 // The user is using the keyboard to change the selection, so stop tracking
191 SetHoveredLine(kNoMatch
);
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
,
199 void OmniboxPopupModel::SetSelectedLineState(LineState state
) {
200 DCHECK(!result().empty());
201 DCHECK_NE(kNoMatch
, selected_line_
);
203 const AutocompleteMatch
& match
= result().match_at(selected_line_
);
204 DCHECK(match
.associated_keyword
.get());
206 selected_line_state_
= state
;
207 view_
->InvalidateLine(selected_line_
);
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
214 if (selected_line_
== kNoMatch
)
217 // Cancel the query so the matches don't change on the user.
218 autocomplete_controller()->Stop(false);
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();
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
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);
242 gfx::Image
OmniboxPopupModel::GetIconIfExtensionMatch(
243 const AutocompleteMatch
& match
) const {
244 Profile
* profile
= edit_model_
->profile();
245 const TemplateURL
* template_url
= match
.GetTemplateURL(profile
, false);
247 (template_url
->GetType() == TemplateURL::OMNIBOX_API_EXTENSION
)) {
248 return extensions::OmniboxAPI::Get(profile
)->GetOmniboxPopupIcon(
249 template_url
->GetExtensionId());
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
);
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());
277 void OmniboxPopupModel::AddObserver(OmniboxPopupModelObserver
* observer
) {
278 observers_
.AddObserver(observer
);
281 void OmniboxPopupModel::RemoveObserver(OmniboxPopupModelObserver
* observer
) {
282 observers_
.RemoveObserver(observer
);