summaryrefslogtreecommitdiffstats
path: root/chrome/browser/autocomplete/autocomplete_edit.cc
blob: 2ab706b305e2c14561d2b80925b7f51385a6a3e6 (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
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
// 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_edit.h"

#include "base/basictypes.h"
#include "base/string_util.h"
#include "chrome/browser/autocomplete/autocomplete_edit_view.h"
#include "chrome/browser/autocomplete/autocomplete_popup_model.h"
#include "chrome/browser/autocomplete/keyword_provider.h"
#include "chrome/browser/metrics/user_metrics.h"
#include "chrome/browser/net/url_fixer_upper.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 "googleurl/src/gurl.h"
#include "googleurl/src/url_util.h"
#include "third_party/skia/include/core/SkBitmap.h"

///////////////////////////////////////////////////////////////////////////////
// AutocompleteEditModel

// A single AutocompleteController used solely for making synchronous calls to
// determine how to deal with the clipboard contents for Paste And Go
// functionality.  We avoid using the popup's controller here because we don't
// want to interrupt in-progress queries or modify the popup state just
// because the user right-clicked the edit.  We don't need a controller for
// every edit because this will always be accessed on the main thread, so we
// won't have thread-safety problems.
static AutocompleteController* paste_and_go_controller = NULL;
static int paste_and_go_controller_refcount = 0;

AutocompleteEditModel::AutocompleteEditModel(
    AutocompleteEditView* view,
    AutocompleteEditController* controller,
    Profile* profile)
    : view_(view),
      controller_(controller),
      has_focus_(false),
      user_input_in_progress_(false),
      just_deleted_text_(false),
      has_temporary_text_(false),
      paste_state_(NONE),
      control_key_state_(UP),
      is_keyword_hint_(false),
      keyword_ui_state_(NORMAL),
      show_search_hint_(true),
      profile_(profile) {
  if (++paste_and_go_controller_refcount == 1) {
    // We don't have a controller yet, so create one.  No profile is set since
    // we'll set this before each call to the controller.
    paste_and_go_controller = new AutocompleteController(NULL);
  }
}

AutocompleteEditModel::~AutocompleteEditModel() {
  if (--paste_and_go_controller_refcount == 0)
    delete paste_and_go_controller;
}

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

const AutocompleteEditModel::State
    AutocompleteEditModel::GetStateForTabSwitch() {
  // Like typing, switching tabs "accepts" the temporary text as the user
  // text, because it makes little sense to have temporary text when the
  // popup is closed.
  if (user_input_in_progress_)
    InternalSetUserText(UserTextFromDisplayText(view_->GetText()));

  return State(user_input_in_progress_, user_text_, keyword_, is_keyword_hint_,
      keyword_ui_state_, show_search_hint_);
}

void AutocompleteEditModel::RestoreState(const State& state) {
  // Restore any user editing.
  if (state.user_input_in_progress) {
    // NOTE: Be sure and set keyword-related state BEFORE invoking
    // DisplayTextFromUserText(), as its result depends upon this state.
    keyword_ = state.keyword;
    is_keyword_hint_ = state.is_keyword_hint;
    keyword_ui_state_ = state.keyword_ui_state;
    show_search_hint_ = state.show_search_hint;
    view_->SetUserText(state.user_text,
        DisplayTextFromUserText(state.user_text), false);
  }
}

bool AutocompleteEditModel::UpdatePermanentText(
    const std::wstring& new_permanent_text) {
  // When there's a new URL, and the user is not editing anything or the edit
  // doesn't have focus, we want to revert the edit to show the new URL.  (The
  // common case where the edit doesn't have focus is when the user has started
  // an edit and then abandoned it and clicked a link on the page.)
  const bool visibly_changed_permanent_text =
      (permanent_text_ != new_permanent_text) &&
      (!user_input_in_progress_ || !has_focus_);

  permanent_text_ = new_permanent_text;
  return visibly_changed_permanent_text;
}

void AutocompleteEditModel::SetUserText(const std::wstring& text) {
  SetInputInProgress(true);
  InternalSetUserText(text);
  paste_state_ = NONE;
  has_temporary_text_ = false;
}

void AutocompleteEditModel::GetDataForURLExport(GURL* url,
                                                std::wstring* title,
                                                SkBitmap* favicon) {
  *url = GetURLForCurrentText(NULL, NULL, NULL);
  if (UTF8ToWide(url->possibly_invalid_spec()) == permanent_text_) {
    *title = controller_->GetTitle();
    *favicon = controller_->GetFavIcon();
  }
}

std::wstring AutocompleteEditModel::GetDesiredTLD() const {
  return (control_key_state_ == DOWN_WITHOUT_CHANGE) ?
    std::wstring(L"com") : std::wstring();
}

bool AutocompleteEditModel::CurrentTextIsURL() {
  // If !user_input_in_progress_, the permanent text is showing, which should
  // always be a URL, so no further checking is needed.  By avoiding checking in
  // this case, we avoid calling into the autocomplete providers, and thus
  // initializing the history system, as long as possible, which speeds startup.
  if (!user_input_in_progress_)
    return true;

  PageTransition::Type transition = PageTransition::LINK;
  GetURLForCurrentText(&transition, NULL, NULL);
  return transition == PageTransition::TYPED;
}

bool AutocompleteEditModel::GetURLForText(const std::wstring& text,
                                          GURL* url) const {
  url_parse::Parsed parts;
  const AutocompleteInput::Type type = AutocompleteInput::Parse(
      UserTextFromDisplayText(text), std::wstring(), &parts, NULL);
  if (type != AutocompleteInput::URL)
    return false;

  *url = GURL(URLFixerUpper::FixupURL(WideToUTF8(text), std::string()));
  return true;
}

void AutocompleteEditModel::SetInputInProgress(bool in_progress) {
  if (user_input_in_progress_ == in_progress)
    return;

  user_input_in_progress_ = in_progress;
  controller_->OnInputInProgress(in_progress);
}

void AutocompleteEditModel::Revert() {
  SetInputInProgress(false);
  paste_state_ = NONE;
  InternalSetUserText(std::wstring());
  keyword_.clear();
  is_keyword_hint_ = false;
  keyword_ui_state_ = NORMAL;
  show_search_hint_ = permanent_text_.empty();
  has_temporary_text_ = false;
  view_->SetWindowTextAndCaretPos(permanent_text_,
                                  has_focus_ ? permanent_text_.length() : 0);
}

void AutocompleteEditModel::StartAutocomplete(
    bool prevent_inline_autocomplete) const {
  popup_->StartAutocomplete(user_text_, GetDesiredTLD(),
      prevent_inline_autocomplete || just_deleted_text_ ||
      (paste_state_ != NONE), keyword_ui_state_ == KEYWORD);
}

bool AutocompleteEditModel::CanPasteAndGo(const std::wstring& text) const {
  // Reset local state.
  paste_and_go_url_ = GURL();
  paste_and_go_transition_ = PageTransition::TYPED;
  paste_and_go_alternate_nav_url_ = GURL();

  // Ask the controller what do do with this input.
  paste_and_go_controller->SetProfile(profile_);
                              // This is cheap, and since there's one
                              // paste_and_go_controller for many tabs which
                              // may all have different profiles, it ensures
                              // we're always using the right one.
  paste_and_go_controller->Start(text, std::wstring(), true, false, true);
  DCHECK(paste_and_go_controller->done());
  const AutocompleteResult& result = paste_and_go_controller->result();
  if (result.empty())
    return false;

  // Set local state based on the default action for this input.
  const AutocompleteResult::const_iterator match(result.default_match());
  DCHECK(match != result.end());
  paste_and_go_url_ = match->destination_url;
  paste_and_go_transition_ = match->transition;
  paste_and_go_alternate_nav_url_ = result.alternate_nav_url();

  return paste_and_go_url_.is_valid();
}

void AutocompleteEditModel::PasteAndGo() {
  // The final parameter to OpenURL, keyword, is not quite correct here: it's
  // possible to "paste and go" a string that contains a keyword.  This is
  // enough of an edge case that we ignore this possibility.
  view_->RevertAll();
  view_->OpenURL(paste_and_go_url_, CURRENT_TAB, paste_and_go_transition_,
      paste_and_go_alternate_nav_url_, AutocompletePopupModel::kNoMatch,
      std::wstring());
}

void AutocompleteEditModel::AcceptInput(WindowOpenDisposition disposition,
                                        bool for_drop) {
  // Get the URL and transition type for the selected entry.
  PageTransition::Type transition;
  bool is_history_what_you_typed_match;
  GURL alternate_nav_url;
  const GURL url(GetURLForCurrentText(&transition,
                                      &is_history_what_you_typed_match,
                                      &alternate_nav_url));
  if (!url.is_valid())
    return;

  if (UTF8ToWide(url.spec()) == permanent_text_) {
    // When the user hit enter on the existing permanent URL, treat it like a
    // reload for scoring purposes.  We could detect this by just checking
    // user_input_in_progress_, but it seems better to treat "edits" that end
    // up leaving the URL unchanged (e.g. deleting the last character and then
    // retyping it) as reloads too.
    transition = PageTransition::RELOAD;
  } else if (for_drop || ((paste_state_ != NONE) &&
                          is_history_what_you_typed_match)) {
    // When the user pasted in a URL and hit enter, score it like a link click
    // rather than a normal typed URL, so it doesn't get inline autocompleted
    // as aggressively later.
    transition = PageTransition::LINK;
  }

  view_->OpenURL(url, disposition, transition, alternate_nav_url,
      AutocompletePopupModel::kNoMatch,
      is_keyword_hint_ ? std::wstring() : keyword_);
}

void AutocompleteEditModel::SendOpenNotification(size_t selected_line,
                                                 const std::wstring& keyword) {
  // We only care about cases where there is a selection (i.e. the popup is
  // open).
  if (popup_->IsOpen()) {
    scoped_ptr<AutocompleteLog> log(popup_->GetAutocompleteLog());
    if (selected_line != AutocompletePopupModel::kNoMatch)
      log->selected_index = selected_line;
    else if (!has_temporary_text_)
      log->inline_autocompleted_length = inline_autocomplete_text_.length();
    NotificationService::current()->Notify(
        NotificationType::OMNIBOX_OPENED_URL, Source<Profile>(profile_),
        Details<AutocompleteLog>(log.get()));
  }

  TemplateURLModel* template_url_model = profile_->GetTemplateURLModel();
  if (keyword.empty() || !template_url_model)
    return;

  const TemplateURL* const template_url =
      template_url_model->GetTemplateURLForKeyword(keyword);
  if (template_url) {
    UserMetrics::RecordAction(L"AcceptedKeyword", profile_);
    template_url_model->IncrementUsageCount(template_url);
  }

  // NOTE: We purposefully don't increment the usage count of the default search
  // engine, if applicable; see comments in template_url.h.
}

void AutocompleteEditModel::AcceptKeyword() {
  view_->OnBeforePossibleChange();
  view_->SetWindowTextAndCaretPos(std::wstring(), 0);
  is_keyword_hint_ = false;
  keyword_ui_state_ = KEYWORD;
  view_->OnAfterPossibleChange();
  just_deleted_text_ = false;  // OnAfterPossibleChange() erroneously sets this
                               // since the edit contents have disappeared.  It
                               // doesn't really matter, but we clear it to be
                               // consistent.
  UserMetrics::RecordAction(L"AcceptedKeywordHint", profile_);
}

void AutocompleteEditModel::ClearKeyword(const std::wstring& visible_text) {
  view_->OnBeforePossibleChange();
  const std::wstring window_text(keyword_ + visible_text);
  view_->SetWindowTextAndCaretPos(window_text.c_str(), keyword_.length());
  keyword_.clear();
  keyword_ui_state_ = NORMAL;
  view_->OnAfterPossibleChange();
  just_deleted_text_ = true;  // OnAfterPossibleChange() fails to clear this
                              // since the edit contents have actually grown
                              // longer.
}

bool AutocompleteEditModel::query_in_progress() const {
  return !popup_->autocomplete_controller()->done();
}

const AutocompleteResult& AutocompleteEditModel::result() const {
  return popup_->autocomplete_controller()->result();
}

void AutocompleteEditModel::OnSetFocus(bool control_down) {
  has_focus_ = true;
  control_key_state_ = control_down ? DOWN_WITHOUT_CHANGE : UP;
}

void AutocompleteEditModel::OnKillFocus() {
  has_focus_ = false;
  control_key_state_ = UP;
  paste_state_ = NONE;

  // Like typing, killing focus "accepts" the temporary text as the user
  // text, because it makes little sense to have temporary text when the
  // popup is closed.
  InternalSetUserText(UserTextFromDisplayText(view_->GetText()));
  has_temporary_text_ = false;
}

bool AutocompleteEditModel::OnEscapeKeyPressed() {
  if (has_temporary_text_ &&
      (popup_->URLsForCurrentSelection(NULL, NULL, NULL) != original_url_)) {
    // The user typed something, then selected a different item.  Restore the
    // text they typed and change back to the default item.
    // NOTE: This purposefully does not reset paste_state_.
    just_deleted_text_ = false;
    has_temporary_text_ = false;
    keyword_ui_state_ = original_keyword_ui_state_;
    popup_->ResetToDefaultMatch();
    view_->OnRevertTemporaryText();
    return true;
  }

  // If the user wasn't editing, but merely had focus in the edit, allow <esc>
  // to be processed as an accelerator, so it can still be used to stop a load.
  // When the permanent text isn't all selected we still fall through to the
  // SelectAll() call below so users can arrow around in the text and then hit
  // <esc> to quickly replace all the text; this matches IE.
  if (!user_input_in_progress_ && view_->IsSelectAll())
    return false;

  view_->RevertAll();
  view_->SelectAll(true);
  return false;
}

void AutocompleteEditModel::OnControlKeyChanged(bool pressed) {
  // Don't change anything unless the key state is actually toggling.
  if (pressed == (control_key_state_ == UP)) {
    control_key_state_ = pressed ? DOWN_WITHOUT_CHANGE : UP;
    if (popup_->IsOpen()) {
      // Autocomplete history provider results may change, so refresh the
      // popup.  This will force user_input_in_progress_ to true, but if the
      // popup is open, that should have already been the case.
      view_->UpdatePopup();
    }
  }
}

void AutocompleteEditModel::OnUpOrDownKeyPressed(int count) {
  // NOTE: This purposefully don't trigger any code that resets paste_state_.

  if (!popup_->IsOpen()) {
    if (popup_->autocomplete_controller()->done()) {
      // The popup is neither open nor working on a query already.  So, start an
      // autocomplete query for the current text.  This also sets
      // user_input_in_progress_ to true, which we want: if the user has started
      // to interact with the popup, changing the permanent_text_ shouldn't
      // change the displayed text.
      // Note: This does not force the popup to open immediately.
      // TODO(pkasting): We should, in fact, force this particular query to open
      // the popup immediately.
      if (!user_input_in_progress_)
        InternalSetUserText(permanent_text_);
      view_->UpdatePopup();
    } else {
      // TODO(pkasting): The popup is working on a query but is not open.  We
      // should force it to open immediately.
    }
  } else {
    // The popup is open, so the user should be able to interact with it
    // normally.
    popup_->Move(count);
  }

  // NOTE: We need to reset the keyword_ui_state_ after the popup updates, since
  // Move() will eventually call back to OnPopupDataChanged(), which needs to
  // save off the current keyword_ui_state_.
  keyword_ui_state_ = NORMAL;
}

void AutocompleteEditModel::OnPopupDataChanged(
    const std::wstring& text,
    bool is_temporary_text,
    const std::wstring& keyword,
    bool is_keyword_hint,
    AutocompleteMatch::Type type) {
  // We don't want to show the search hint if we're showing a keyword hint or
  // selected keyword, or (subtle!) if we would be showing a selected keyword
  // but for keyword_ui_state_ == NO_KEYWORD.
  const bool show_search_hint = keyword.empty() &&
      ((type == AutocompleteMatch::SEARCH_WHAT_YOU_TYPED) ||
       (type == AutocompleteMatch::SEARCH_HISTORY) ||
       (type == AutocompleteMatch::SEARCH_SUGGEST));

  // Update keyword/hint-related local state.
  bool keyword_state_changed = (keyword_ != keyword) ||
      ((is_keyword_hint_ != is_keyword_hint) && !keyword.empty()) ||
      (show_search_hint_ != show_search_hint);
  if (keyword_state_changed) {
    keyword_ = keyword;
    is_keyword_hint_ = is_keyword_hint;
    show_search_hint_ = show_search_hint;
  }

  // Handle changes to temporary text.
  if (is_temporary_text) {
    const bool save_original_selection = !has_temporary_text_;
    if (save_original_selection) {
      // Save the original selection and URL so it can be reverted later.
      has_temporary_text_ = true;
      original_url_ = popup_->URLsForCurrentSelection(NULL, NULL, NULL);
      original_keyword_ui_state_ = keyword_ui_state_;
    }
    view_->OnTemporaryTextMaybeChanged(DisplayTextFromUserText(text),
                                       save_original_selection);
    return;
  }

  // Handle changes to inline autocomplete text.  Don't make changes if the user
  // is showing temporary text.  Making display changes would be obviously
  // wrong; making changes to the inline_autocomplete_text_ itself turns out to
  // be more subtlely wrong, because it means hitting esc will no longer revert
  // to the original state before arrowing.
  if (!has_temporary_text_) {
    inline_autocomplete_text_ = text;
    if (view_->OnInlineAutocompleteTextMaybeChanged(
        DisplayTextFromUserText(user_text_ + inline_autocomplete_text_),
        DisplayTextFromUserText(user_text_).length()))
      return;
  }

  // If the above changes didn't warrant a text update but we did change keyword
  // state, we have yet to notify the controller about it.
  if (keyword_state_changed)
    controller_->OnChanged();
}

bool AutocompleteEditModel::OnAfterPossibleChange(const std::wstring& new_text,
                                                  bool selection_differs,
                                                  bool text_differs,
                                                  bool just_deleted_text,
                                                  bool at_end_of_edit) {
  // Update the paste state as appropriate: if we're just finishing a paste
  // that replaced all the text, preserve that information; otherwise, if we've
  // made some other edit, clear paste tracking.
  if (paste_state_ == REPLACING_ALL)
    paste_state_ = REPLACED_ALL;
  else if (text_differs)
    paste_state_ = NONE;

  // If something has changed while the control key is down, prevent
  // "ctrl-enter" until the control key is released.  When we do this, we need
  // to update the popup if it's open, since the desired_tld will have changed.
  if ((text_differs || selection_differs) &&
      (control_key_state_ == DOWN_WITHOUT_CHANGE)) {
    control_key_state_ = DOWN_WITH_CHANGE;
    if (!text_differs && !popup_->IsOpen())
      return false;  // Don't open the popup for no reason.
  } else if (!text_differs &&
             (inline_autocomplete_text_.empty() || !selection_differs)) {
    return false;
  }

  const bool had_keyword = (keyword_ui_state_ != NO_KEYWORD) &&
      !is_keyword_hint_ && !keyword_.empty();

  // Modifying the selection counts as accepting the autocompleted text.
  InternalSetUserText(UserTextFromDisplayText(new_text));
  has_temporary_text_ = false;

  // Track when the user has deleted text so we won't allow inline autocomplete.
  just_deleted_text_ = just_deleted_text;

  // Disable the fancy keyword UI if the user didn't already have a visible
  // keyword and is not at the end of the edit.  This prevents us from showing
  // the fancy UI (and interrupting the user's editing) if the user happens to
  // have a keyword for 'a', types 'ab' then puts a space between the 'a' and
  // the 'b'.
  if (!had_keyword)
    keyword_ui_state_ = at_end_of_edit ? NORMAL : NO_KEYWORD;

  view_->UpdatePopup();

  if (had_keyword) {
    if (is_keyword_hint_ || keyword_.empty())
      keyword_ui_state_ = NORMAL;
  } else if ((keyword_ui_state_ != NO_KEYWORD) && !is_keyword_hint_ &&
             !keyword_.empty()) {
    // Went from no selected keyword to a selected keyword.
    keyword_ui_state_ = KEYWORD;
  }

  return true;
}

void AutocompleteEditModel::InternalSetUserText(const std::wstring& text) {
  user_text_ = text;
  just_deleted_text_ = false;
  inline_autocomplete_text_.clear();
}

std::wstring AutocompleteEditModel::DisplayTextFromUserText(
    const std::wstring& text) const {
  return ((keyword_ui_state_ == NO_KEYWORD) || is_keyword_hint_ ||
          keyword_.empty()) ?
      text : KeywordProvider::SplitReplacementStringFromInput(text);
}

std::wstring AutocompleteEditModel::UserTextFromDisplayText(
    const std::wstring& text) const {
  return ((keyword_ui_state_ == NO_KEYWORD) || is_keyword_hint_ ||
          keyword_.empty()) ?
      text : (keyword_ + L" " + text);
}

GURL AutocompleteEditModel::GetURLForCurrentText(
    PageTransition::Type* transition,
    bool* is_history_what_you_typed_match,
    GURL* alternate_nav_url) {
  return (popup_->IsOpen() || !popup_->autocomplete_controller()->done()) ?
      popup_->URLsForCurrentSelection(transition,
                                      is_history_what_you_typed_match,
                                      alternate_nav_url) :
      popup_->URLsForDefaultMatch(UserTextFromDisplayText(view_->GetText()),
                                  GetDesiredTLD(), transition,
                                  is_history_what_you_typed_match,
                                  alternate_nav_url);
}