summaryrefslogtreecommitdiffstats
path: root/chrome/browser/autocomplete/autocomplete_popup_model.cc
blob: 0186240ee8fa45f190a0e3beb578e862e3cd5d31 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/autocomplete/autocomplete_popup_model.h"

#include "base/string_util.h"
#include "chrome/browser/autocomplete/autocomplete_edit.h"
#include "chrome/browser/autocomplete/autocomplete_popup_view.h"
#include "chrome/browser/net/dns_global.h"
#include "chrome/browser/profile.h"
#include "chrome/browser/search_engines/template_url.h"
#include "chrome/browser/search_engines/template_url_model.h"
#include "chrome/common/notification_service.h"
#include "third_party/icu38/public/common/unicode/ubidi.h"

AutocompletePopupModel::AutocompletePopupModel(
    AutocompletePopupView* popup_view,
    AutocompleteEditModel* edit_model,
    Profile* profile)
    : view_(popup_view),
      edit_model_(edit_model),
      controller_(new AutocompleteController(profile)),
      profile_(profile),
      hovered_line_(kNoMatch),
      selected_line_(kNoMatch),
      inside_synchronous_query_(false) {
  registrar_.Add(
      this,
      NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
      Source<AutocompleteController>(controller_.get()));
  registrar_.Add(
      this,
      NotificationType::AUTOCOMPLETE_CONTROLLER_SYNCHRONOUS_MATCHES_AVAILABLE,
      Source<AutocompleteController>(controller_.get()));
}

AutocompletePopupModel::~AutocompletePopupModel() {
  StopAutocomplete();
}

void AutocompletePopupModel::SetProfile(Profile* profile) {
  DCHECK(profile);
  profile_ = profile;
  controller_->SetProfile(profile);
}

void AutocompletePopupModel::StartAutocomplete(
    const std::wstring& text,
    const std::wstring& desired_tld,
    bool prevent_inline_autocomplete,
    bool prefer_keyword) {
  // The user is interacting with the edit, so stop tracking hover.
  SetHoveredLine(kNoMatch);

  manually_selected_match_.Clear();

  controller_->Start(text, desired_tld, prevent_inline_autocomplete,
                     prefer_keyword, false);
}

void AutocompletePopupModel::StopAutocomplete() {
  controller_->Stop(true);
  SetHoveredLine(kNoMatch);
  selected_line_ = kNoMatch;
  view_->UpdatePopupAppearance();
}

bool AutocompletePopupModel::IsOpen() const {
  return view_->IsOpen();
}

void AutocompletePopupModel::SetHoveredLine(size_t line) {
  const bool is_disabling = (line == kNoMatch);
  DCHECK(is_disabling || (line < controller_->result().size()));

  if (line == hovered_line_)
    return;  // Nothing to do

  // Make sure the old hovered line is redrawn.  No need to redraw the selected
  // line since selection overrides hover so the appearance won't change.
  const bool is_enabling = (hovered_line_ == kNoMatch);
  if (!is_enabling && (hovered_line_ != selected_line_))
    view_->InvalidateLine(hovered_line_);

  // Change the hover to the new line and make sure it's redrawn.
  hovered_line_ = line;
  if (!is_disabling && (hovered_line_ != selected_line_))
    view_->InvalidateLine(hovered_line_);

  if (is_enabling || is_disabling)
    view_->OnHoverEnabledOrDisabled(is_disabling);
}

void AutocompletePopupModel::SetSelectedLine(size_t line,
                                             bool reset_to_default) {
  const AutocompleteResult& result = controller_->result();
  DCHECK(line < result.size());
  if (result.empty())
    return;

  // Cancel the query so the matches don't change on the user.
  controller_->Stop(false);

  const AutocompleteMatch& match = result.match_at(line);
  if (reset_to_default) {
    manually_selected_match_.Clear();
  } else {
    // Track the user's selection until they cancel it.
    manually_selected_match_.destination_url = match.destination_url;
    manually_selected_match_.provider_affinity = match.provider;
    manually_selected_match_.is_history_what_you_typed_match =
        match.is_history_what_you_typed_match;
  }

  if (line == selected_line_)
    return;  // Nothing else to do.

  // Update the edit with the new data for this match.
  std::wstring keyword;
  const bool is_keyword_hint = GetKeywordForMatch(match, &keyword);
  edit_model_->OnPopupDataChanged(
      reset_to_default ? std::wstring() : match.fill_into_edit,
      !reset_to_default, keyword, is_keyword_hint, match.type);

  // Repaint old and new selected lines immediately, so that the edit doesn't
  // appear to update [much] faster than the popup.  We must not update
  // |selected_line_| before calling OnPopupDataChanged() (since the edit may
  // call us back to get data about the old selection), and we must not call
  // UpdateWindow() before updating |selected_line_| (since the paint routine
  // relies on knowing the correct selected line).
  view_->InvalidateLine(selected_line_);
  selected_line_ = line;
  view_->InvalidateLine(selected_line_);
  view_->PaintUpdatesNow();
}

void AutocompletePopupModel::ResetToDefaultMatch() {
  const AutocompleteResult& result = controller_->result();
  DCHECK(!result.empty());
  SetSelectedLine(result.default_match() - result.begin(), true);
}

GURL AutocompletePopupModel::URLsForCurrentSelection(
    PageTransition::Type* transition,
    bool* is_history_what_you_typed_match,
    GURL* alternate_nav_url) const {
  // We need to use the result on the controller, because if the popup is open,
  // the user changes the contents of the edit, and then presses enter before
  // any results have been displayed, results_ will be nonempty but wrong.  (In
  // most other cases, the controller's results will match the popup's.)
  // TODO(pkasting): If manually_selected_match_ moves to the controller, this
  // can move to the edit.
  if (controller_->result().empty())
    return GURL();

  const AutocompleteResult& result = controller_->result();
  AutocompleteResult::const_iterator match;
  if (!controller_->done()) {
    // The user cannot have manually selected a match, or the query would have
    // stopped.  So the default match must be the desired selection.
    match = result.default_match();
  } else {
    // The query isn't running, so the popup can't possibly be out of date.
    DCHECK(selected_line_ < result.size());
    match = result.begin() + selected_line_;
  }
  if (transition)
    *transition = match->transition;
  if (is_history_what_you_typed_match)
    *is_history_what_you_typed_match = match->is_history_what_you_typed_match;
  if (alternate_nav_url && manually_selected_match_.empty())
    *alternate_nav_url = result.alternate_nav_url();
  return match->destination_url;
}

GURL AutocompletePopupModel::URLsForDefaultMatch(
    const std::wstring& text,
    const std::wstring& desired_tld,
    PageTransition::Type* transition,
    bool* is_history_what_you_typed_match,
    GURL* alternate_nav_url) {
  // We had better not already be doing anything, or this call will blow it
  // away.
  DCHECK(!IsOpen());
  DCHECK(controller_->done());

  // Run the new query and get only the synchronously available matches.
  inside_synchronous_query_ = true;  // Tell Observe() not to notify the edit or
                                     // update our appearance.
  controller_->Start(text, desired_tld, true, false, true);
  inside_synchronous_query_ = false;
  DCHECK(controller_->done());
  const AutocompleteResult& result = controller_->result();
  if (result.empty())
    return GURL();

  // Get the URLs for the default match.
  const AutocompleteResult::const_iterator match = result.default_match();
  if (transition)
    *transition = match->transition;
  if (is_history_what_you_typed_match)
    *is_history_what_you_typed_match = match->is_history_what_you_typed_match;
  if (alternate_nav_url)
    *alternate_nav_url = result.alternate_nav_url();
  return match->destination_url;
}

bool AutocompletePopupModel::GetKeywordForMatch(const AutocompleteMatch& match,
                                                std::wstring* keyword) const {
  // Assume we have no keyword until we find otherwise.
  keyword->clear();

  // If the current match is a keyword, return that as the selected keyword.
  if (TemplateURL::SupportsReplacement(match.template_url)) {
    keyword->assign(match.template_url->keyword());
    return false;
  }

  // See if the current match's fill_into_edit corresponds to a keyword.
  if (!profile_->GetTemplateURLModel())
    return false;
  profile_->GetTemplateURLModel()->Load();
  const std::wstring keyword_hint(
      TemplateURLModel::CleanUserInputKeyword(match.fill_into_edit));
  if (keyword_hint.empty())
    return false;

  // Don't provide a hint if this keyword doesn't support replacement.
  const TemplateURL* const template_url =
      profile_->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword_hint);
  if (!TemplateURL::SupportsReplacement(template_url))
    return false;

  keyword->assign(keyword_hint);
  return true;
}

AutocompleteLog* AutocompletePopupModel::GetAutocompleteLog() {
  return new AutocompleteLog(controller_->input().text(),
      controller_->input().type(), selected_line_, 0, controller_->result());
}

void AutocompletePopupModel::Move(int count) {
  // TODO(pkasting): Temporary hack.  If the query is running while the popup is
  // open, we might be showing the results of the previous query still.  Force
  // the popup to display the latest results so the popup and the controller
  // aren't out of sync.  The better fix here is to roll the controller back to
  // be in sync with what the popup is showing.
  if (IsOpen() && !controller_->done()) {
    Observe(NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
            Source<AutocompleteController>(controller_.get()),
            NotificationService::NoDetails());
  }

  const AutocompleteResult& result = controller_->result();
  if (result.empty())
    return;

  // The user is using the keyboard to change the selection, so stop tracking
  // hover.
  SetHoveredLine(kNoMatch);

  // Clamp the new line to [0, result_.count() - 1].
  const size_t new_line = selected_line_ + count;
  SetSelectedLine((((count < 0) && (new_line >= selected_line_)) ?
      0 : std::min(new_line, result.size() - 1)), false);
}

void AutocompletePopupModel::TryDeletingCurrentItem() {
  // We could use URLsForCurrentSelection() here, but it seems better to try
  // and shift-delete the actual selection, rather than any "in progress, not
  // yet visible" one.
  if (selected_line_ == kNoMatch)
    return;
  const AutocompleteMatch& match =
      controller_->result().match_at(selected_line_);
  if (match.deletable) {
    const size_t selected_line = selected_line_;
    controller_->DeleteMatch(match);  // This will synchronously notify us that
                                      // the results have changed.
    const AutocompleteResult& result = controller_->result();
    if (!result.empty()) {
      // Move the selection to the next choice after the deleted one.
      // TODO(pkasting): Eventually the controller should take care of this
      // before notifying us, reducing flicker.  At that point the check for
      // deletability can move there too.
      SetSelectedLine(std::min(result.size() - 1, selected_line), false);
    }
  }
}

void AutocompletePopupModel::Observe(NotificationType type,
                                     const NotificationSource& source,
                                     const NotificationDetails& details) {
  if (inside_synchronous_query_)
    return;

  const AutocompleteResult& result = controller_->result();
  switch (type.value) {
    case NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED: {
      selected_line_ = (result.default_match() == result.end()) ?
          kNoMatch : (result.default_match() - result.begin());
      // If we're going to trim the window size to no longer include the hovered
      // line, turn hover off.  Practically, this shouldn't happen, but it
      // doesn't hurt to be defensive.
      if ((hovered_line_ != kNoMatch) && (result.size() <= hovered_line_))
        SetHoveredLine(kNoMatch);

      view_->UpdatePopupAppearance();
    }
    // FALL THROUGH

    case NotificationType::
             AUTOCOMPLETE_CONTROLLER_SYNCHRONOUS_MATCHES_AVAILABLE: {
      // Update the edit with the possibly new data for this match.
      // NOTE: This must be done after the code above, so that our internal
      // state will be consistent when the edit calls back to
      // URLsForCurrentSelection().
      std::wstring inline_autocomplete_text;
      std::wstring keyword;
      bool is_keyword_hint = false;
      AutocompleteMatch::Type type = AutocompleteMatch::SEARCH_WHAT_YOU_TYPED;
      const AutocompleteResult::const_iterator match(result.default_match());
      if (match != result.end()) {
        if ((match->inline_autocomplete_offset != std::wstring::npos) &&
            (match->inline_autocomplete_offset <
                match->fill_into_edit.length())) {
          inline_autocomplete_text =
              match->fill_into_edit.substr(match->inline_autocomplete_offset);
        }
        // Warm up DNS Prefetch Cache.
        chrome_browser_net::DnsPrefetchUrl(match->destination_url);
        // We could prefetch the alternate nav URL, if any, but because there
        // can be many of these as a user types an initial series of characters,
        // the OS DNS cache could suffer eviction problems for minimal gain.

        is_keyword_hint = GetKeywordForMatch(*match, &keyword);
        type = match->type;
      }
      edit_model_->OnPopupDataChanged(inline_autocomplete_text, false, keyword,
          is_keyword_hint, type);
      return;
    }

    default:
      NOTREACHED();
  }
}