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