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
26const size_t OmniboxPopupModel::kNoMatch = -1;
27
28OmniboxPopupModel::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
39OmniboxPopupModel::~OmniboxPopupModel() {
40}
41
42bool OmniboxPopupModel::IsOpen() const {
43  return view_->IsOpen();
44}
45
46void 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
64void 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
126void 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
133void 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
148void 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
159void 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
191gfx::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
203void 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
226void OmniboxPopupModel::AddObserver(OmniboxPopupModelObserver* observer) {
227  observers_.AddObserver(observer);
228}
229
230void OmniboxPopupModel::RemoveObserver(OmniboxPopupModelObserver* observer) {
231  observers_.RemoveObserver(observer);
232}
233