普通文本  |  651行  |  21.41 KB

// Copyright (c) 2011 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_view_views.h"

#include "base/logging.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/autocomplete/autocomplete_edit.h"
#include "chrome/browser/autocomplete/autocomplete_match.h"
#include "chrome/browser/autocomplete/autocomplete_popup_model.h"
#include "chrome/browser/command_updater.h"
#include "chrome/browser/ui/views/autocomplete/autocomplete_popup_contents_view.h"
#include "chrome/browser/ui/views/autocomplete/touch_autocomplete_popup_contents_view.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_service.h"
#include "googleurl/src/gurl.h"
#include "grit/generated_resources.h"
#include "net/base/escape.h"
#include "ui/base/accessibility/accessible_view_state.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/font.h"
#include "views/border.h"
#include "views/controls/textfield/textfield.h"
#include "views/layout/fill_layout.h"

namespace {

// Textfield for autocomplete that intercepts events that are necessary
// for AutocompleteEditViewViews.
class AutocompleteTextfield : public views::Textfield {
 public:
  explicit AutocompleteTextfield(
      AutocompleteEditViewViews* autocomplete_edit_view)
      : views::Textfield(views::Textfield::STYLE_DEFAULT),
        autocomplete_edit_view_(autocomplete_edit_view) {
    DCHECK(autocomplete_edit_view_);
    RemoveBorder();
  }

  // views::View implementation
  virtual void OnFocus() OVERRIDE {
    views::Textfield::OnFocus();
    autocomplete_edit_view_->HandleFocusIn();
  }

  virtual void OnBlur() OVERRIDE {
    views::Textfield::OnBlur();
    autocomplete_edit_view_->HandleFocusOut();
  }

  virtual bool OnKeyPressed(const views::KeyEvent& event) OVERRIDE {
    bool handled = views::Textfield::OnKeyPressed(event);
    return autocomplete_edit_view_->HandleAfterKeyEvent(event, handled) ||
        handled;
  }

  virtual bool OnKeyReleased(const views::KeyEvent& event) OVERRIDE {
    return autocomplete_edit_view_->HandleKeyReleaseEvent(event);
  }

  virtual bool IsFocusable() const OVERRIDE {
    // Bypass Textfield::IsFocusable. The omnibox in popup window requires
    // focus in order for text selection to work.
    return views::View::IsFocusable();
  }

 private:
  AutocompleteEditViewViews* autocomplete_edit_view_;

  DISALLOW_COPY_AND_ASSIGN(AutocompleteTextfield);
};

// Stores omnibox state for each tab.
struct ViewState {
  explicit ViewState(const ui::Range& selection_range)
      : selection_range(selection_range) {
  }

  // Range of selected text.
  ui::Range selection_range;
};

struct AutocompleteEditState {
  AutocompleteEditState(const AutocompleteEditModel::State& model_state,
                        const ViewState& view_state)
      : model_state(model_state),
        view_state(view_state) {
  }

  const AutocompleteEditModel::State model_state;
  const ViewState view_state;
};

// Returns a lazily initialized property bag accessor for saving our state in a
// TabContents.
PropertyAccessor<AutocompleteEditState>* GetStateAccessor() {
  static PropertyAccessor<AutocompleteEditState> state;
  return &state;
}

const int kAutocompleteVerticalMargin = 4;

}  // namespace

AutocompleteEditViewViews::AutocompleteEditViewViews(
    AutocompleteEditController* controller,
    ToolbarModel* toolbar_model,
    Profile* profile,
    CommandUpdater* command_updater,
    bool popup_window_mode,
    const views::View* location_bar)
    : model_(new AutocompleteEditModel(this, controller, profile)),
      popup_view_(CreatePopupView(profile, location_bar)),
      controller_(controller),
      toolbar_model_(toolbar_model),
      command_updater_(command_updater),
      popup_window_mode_(popup_window_mode),
      security_level_(ToolbarModel::NONE),
      ime_composing_before_change_(false),
      delete_at_end_pressed_(false) {
  set_border(views::Border::CreateEmptyBorder(kAutocompleteVerticalMargin, 0,
                                              kAutocompleteVerticalMargin, 0));
}

AutocompleteEditViewViews::~AutocompleteEditViewViews() {
  NotificationService::current()->Notify(
      NotificationType::AUTOCOMPLETE_EDIT_DESTROYED,
      Source<AutocompleteEditViewViews>(this),
      NotificationService::NoDetails());
  // Explicitly teardown members which have a reference to us.  Just to be safe
  // we want them to be destroyed before destroying any other internal state.
  popup_view_.reset();
  model_.reset();
}

////////////////////////////////////////////////////////////////////////////////
// AutocompleteEditViewViews public:

void AutocompleteEditViewViews::Init() {
  // The height of the text view is going to change based on the font used.  We
  // don't want to stretch the height, and we want it vertically centered.
  // TODO(oshima): make sure the above happens with views.
  textfield_ = new AutocompleteTextfield(this);
  textfield_->SetController(this);

#if defined(TOUCH_UI)
  textfield_->SetFont(ui::ResourceBundle::GetSharedInstance().GetFont(
                      ResourceBundle::LargeFont));
#endif

  if (popup_window_mode_)
    textfield_->SetReadOnly(true);

  // Manually invoke SetBaseColor() because TOOLKIT_VIEWS doesn't observe
  // themes.
  SetBaseColor();
}

void AutocompleteEditViewViews::SetBaseColor() {
  // TODO(oshima): Implment style change.
  NOTIMPLEMENTED();
}

bool AutocompleteEditViewViews::HandleAfterKeyEvent(
    const views::KeyEvent& event,
    bool handled) {
  if (event.key_code() == ui::VKEY_RETURN) {
    bool alt_held = event.IsAltDown();
    model_->AcceptInput(alt_held ? NEW_FOREGROUND_TAB : CURRENT_TAB, false);
    handled = true;
  } else if (!handled && event.key_code() == ui::VKEY_ESCAPE) {
    // We can handle the Escape key if textfield did not handle it.
    // If it's not handled by us, then we need to propagate it up to the parent
    // widgets, so that Escape accelerator can still work.
    handled = model_->OnEscapeKeyPressed();
  } else if (event.key_code() == ui::VKEY_CONTROL) {
    // Omnibox2 can switch its contents while pressing a control key. To switch
    // the contents of omnibox2, we notify the AutocompleteEditModel class when
    // the control-key state is changed.
    model_->OnControlKeyChanged(true);
  } else if (!handled && event.key_code() == ui::VKEY_DELETE &&
             event.IsShiftDown()) {
    // If shift+del didn't change the text, we let this delete an entry from
    // the popup.  We can't check to see if the IME handled it because even if
    // nothing is selected, the IME or the TextView still report handling it.
    if (model_->popup_model()->IsOpen())
      model_->popup_model()->TryDeletingCurrentItem();
  } else if (!handled && event.key_code() == ui::VKEY_UP) {
    model_->OnUpOrDownKeyPressed(-1);
    handled = true;
  } else if (!handled && event.key_code() == ui::VKEY_DOWN) {
    model_->OnUpOrDownKeyPressed(1);
    handled = true;
  } else if (!handled &&
             event.key_code() == ui::VKEY_TAB &&
             !event.IsShiftDown() &&
             !event.IsControlDown()) {
    if (model_->is_keyword_hint()) {
      handled = model_->AcceptKeyword();
    } else {
      string16::size_type start = 0;
      string16::size_type end = 0;
      size_t length = GetTextLength();
      GetSelectionBounds(&start, &end);
      if (start != end || start < length) {
        OnBeforePossibleChange();
        SelectRange(length, length);
        OnAfterPossibleChange();
        handled = true;
      }

      // TODO(Oshima): handle instant
    }
  }
  // TODO(oshima): page up & down

  return handled;
}

bool AutocompleteEditViewViews::HandleKeyReleaseEvent(
    const views::KeyEvent& event) {
  // Omnibox2 can switch its contents while pressing a control key. To switch
  // the contents of omnibox2, we notify the AutocompleteEditModel class when
  // the control-key state is changed.
  if (event.key_code() == ui::VKEY_CONTROL) {
    // TODO(oshima): investigate if we need to support keyboard with two
    // controls. See autocomplete_edit_view_gtk.cc.
    model_->OnControlKeyChanged(false);
    return true;
  }
  return false;
}

void AutocompleteEditViewViews::HandleFocusIn() {
  // TODO(oshima): Get control key state.
  model_->OnSetFocus(false);
  // Don't call controller_->OnSetFocus as this view has already
  // acquired the focus.
}

void AutocompleteEditViewViews::HandleFocusOut() {
  // TODO(oshima): we don't have native view. This requires
  // further refactoring.
  model_->OnWillKillFocus(NULL);
  // Close the popup.
  ClosePopup();
  // Tell the model to reset itself.
  model_->OnKillFocus();
  controller_->OnKillFocus();
}

////////////////////////////////////////////////////////////////////////////////
// AutocompleteEditViewViews, views::View implementation:
void AutocompleteEditViewViews::Layout() {
  gfx::Insets insets = GetInsets();
  textfield_->SetBounds(insets.left(), insets.top(),
                        width() - insets.width(),
                        height() - insets.height());
}

void AutocompleteEditViewViews::GetAccessibleState(
    ui::AccessibleViewState* state) {
  state->name = l10n_util::GetStringUTF16(IDS_ACCNAME_LOCATION);
}

////////////////////////////////////////////////////////////////////////////////
// AutocompleteEditViewViews, AutocopmleteEditView implementation:

AutocompleteEditModel* AutocompleteEditViewViews::model() {
  return model_.get();
}

const AutocompleteEditModel* AutocompleteEditViewViews::model() const {
  return model_.get();
}

void AutocompleteEditViewViews::SaveStateToTab(TabContents* tab) {
  DCHECK(tab);

  // NOTE: GetStateForTabSwitch may affect GetSelection, so order is important.
  AutocompleteEditModel::State model_state = model_->GetStateForTabSwitch();
  ui::Range selection;
  textfield_->GetSelectedRange(&selection);
  GetStateAccessor()->SetProperty(
      tab->property_bag(),
      AutocompleteEditState(model_state, ViewState(selection)));
}

void AutocompleteEditViewViews::Update(const TabContents* contents) {
  // NOTE: We're getting the URL text here from the ToolbarModel.
  bool visibly_changed_permanent_text =
      model_->UpdatePermanentText(WideToUTF16Hack(toolbar_model_->GetText()));

  ToolbarModel::SecurityLevel security_level =
        toolbar_model_->GetSecurityLevel();
  bool changed_security_level = (security_level != security_level_);
  security_level_ = security_level;

  // TODO(oshima): Copied from gtk implementation which is
  // slightly different from WIN impl. Find out the correct implementation
  // for views-implementation.
  if (contents) {
    RevertAll();
    const AutocompleteEditState* state =
        GetStateAccessor()->GetProperty(contents->property_bag());
    if (state) {
      model_->RestoreState(state->model_state);

      // Move the marks for the cursor and the other end of the selection to
      // the previously-saved offsets (but preserve PRIMARY).
      textfield_->SelectRange(state->view_state.selection_range);
    }
  } else if (visibly_changed_permanent_text) {
    RevertAll();
  } else if (changed_security_level) {
    EmphasizeURLComponents();
  }
}

void AutocompleteEditViewViews::OpenURL(const GURL& url,
                                        WindowOpenDisposition disposition,
                                        PageTransition::Type transition,
                                        const GURL& alternate_nav_url,
                                        size_t selected_line,
                                        const string16& keyword) {
  if (!url.is_valid())
    return;

  model_->OpenURL(url, disposition, transition, alternate_nav_url,
                  selected_line, keyword);
}

string16 AutocompleteEditViewViews::GetText() const {
  // TODO(oshima): IME support
  return textfield_->text();
}

bool AutocompleteEditViewViews::IsEditingOrEmpty() const {
  return model_->user_input_in_progress() || (GetTextLength() == 0);
}

int AutocompleteEditViewViews::GetIcon() const {
  return IsEditingOrEmpty() ?
      AutocompleteMatch::TypeToIcon(model_->CurrentTextType()) :
      toolbar_model_->GetIcon();
}

void AutocompleteEditViewViews::SetUserText(const string16& text) {
  SetUserText(text, text, true);
}

void AutocompleteEditViewViews::SetUserText(const string16& text,
                                            const string16& display_text,
                                            bool update_popup) {
  model_->SetUserText(text);
  SetWindowTextAndCaretPos(display_text, display_text.length());
  if (update_popup)
    UpdatePopup();
  TextChanged();
}

void AutocompleteEditViewViews::SetWindowTextAndCaretPos(
    const string16& text,
    size_t caret_pos) {
  const ui::Range range(caret_pos, caret_pos);
  SetTextAndSelectedRange(text, range);
}

void AutocompleteEditViewViews::SetForcedQuery() {
  const string16 current_text(GetText());
  const size_t start = current_text.find_first_not_of(kWhitespaceUTF16);
  if (start == string16::npos || (current_text[start] != '?')) {
    SetUserText(ASCIIToUTF16("?"));
  } else {
    SelectRange(current_text.size(), start + 1);
  }
}

bool AutocompleteEditViewViews::IsSelectAll() {
  // TODO(oshima): IME support.
  return textfield_->text() == textfield_->GetSelectedText();
}

bool AutocompleteEditViewViews::DeleteAtEndPressed() {
  return delete_at_end_pressed_;
}

void AutocompleteEditViewViews::GetSelectionBounds(
    string16::size_type* start,
    string16::size_type* end) {
  ui::Range range;
  textfield_->GetSelectedRange(&range);
  *start = static_cast<size_t>(range.end());
  *end = static_cast<size_t>(range.start());
}

void AutocompleteEditViewViews::SelectAll(bool reversed) {
  if (reversed)
    SelectRange(GetTextLength(), 0);
  else
    SelectRange(0, GetTextLength());
}

void AutocompleteEditViewViews::RevertAll() {
  ClosePopup();
  model_->Revert();
  TextChanged();
}

void AutocompleteEditViewViews::UpdatePopup() {
  model_->SetInputInProgress(true);
  if (!model_->has_focus())
    return;

  // Don't inline autocomplete when the caret/selection isn't at the end of
  // the text, or in the middle of composition.
  ui::Range sel;
  textfield_->GetSelectedRange(&sel);
  bool no_inline_autocomplete =
      sel.GetMax() < GetTextLength() || textfield_->IsIMEComposing();

  model_->StartAutocomplete(!sel.is_empty(), no_inline_autocomplete);
}

void AutocompleteEditViewViews::ClosePopup() {
  model_->StopAutocomplete();
}

void AutocompleteEditViewViews::SetFocus() {
  // In views-implementation, the focus is on textfield rather than
  // AutocompleteEditView.
  textfield_->RequestFocus();
}

void AutocompleteEditViewViews::OnTemporaryTextMaybeChanged(
    const string16& display_text,
    bool save_original_selection) {
  if (save_original_selection)
    textfield_->GetSelectedRange(&saved_temporary_selection_);

  SetWindowTextAndCaretPos(display_text, display_text.length());
  TextChanged();
}

bool AutocompleteEditViewViews::OnInlineAutocompleteTextMaybeChanged(
    const string16& display_text,
    size_t user_text_length) {
  if (display_text == GetText())
    return false;
  ui::Range range(display_text.size(), user_text_length);
  SetTextAndSelectedRange(display_text, range);
  TextChanged();
  return true;
}

void AutocompleteEditViewViews::OnRevertTemporaryText() {
  textfield_->SelectRange(saved_temporary_selection_);
  TextChanged();
}

void AutocompleteEditViewViews::OnBeforePossibleChange() {
  // Record our state.
  text_before_change_ = GetText();
  textfield_->GetSelectedRange(&sel_before_change_);
  ime_composing_before_change_ = textfield_->IsIMEComposing();
}

bool AutocompleteEditViewViews::OnAfterPossibleChange() {
  ui::Range new_sel;
  textfield_->GetSelectedRange(&new_sel);

  // See if the text or selection have changed since OnBeforePossibleChange().
  const string16 new_text = GetText();
  const bool text_changed = (new_text != text_before_change_) ||
      (ime_composing_before_change_ != textfield_->IsIMEComposing());
  const bool selection_differs =
      !((sel_before_change_.is_empty() && new_sel.is_empty()) ||
        sel_before_change_.EqualsIgnoringDirection(new_sel));

  // When the user has deleted text, we don't allow inline autocomplete.  Make
  // sure to not flag cases like selecting part of the text and then pasting
  // (or typing) the prefix of that selection.  (We detect these by making
  // sure the caret, which should be after any insertion, hasn't moved
  // forward of the old selection start.)
  const bool just_deleted_text =
      (text_before_change_.length() > new_text.length()) &&
      (new_sel.start() <= sel_before_change_.GetMin());

  const bool something_changed = model_->OnAfterPossibleChange(
      new_text, new_sel.start(), new_sel.end(), selection_differs,
      text_changed, just_deleted_text, !textfield_->IsIMEComposing());

  // If only selection was changed, we don't need to call |model_|'s
  // OnChanged() method, which is called in TextChanged().
  // But we still need to call EmphasizeURLComponents() to make sure the text
  // attributes are updated correctly.
  if (something_changed && text_changed)
    TextChanged();
  else if (selection_differs)
    EmphasizeURLComponents();
  else if (delete_at_end_pressed_)
    model_->OnChanged();

  return something_changed;
}

gfx::NativeView AutocompleteEditViewViews::GetNativeView() const {
  return GetWidget()->GetNativeView();
}

CommandUpdater* AutocompleteEditViewViews::GetCommandUpdater() {
  return command_updater_;
}

void AutocompleteEditViewViews::SetInstantSuggestion(const string16& input,
                                                     bool animate_to_complete) {
  NOTIMPLEMENTED();
}

string16 AutocompleteEditViewViews::GetInstantSuggestion() const {
  NOTIMPLEMENTED();
  return string16();
}

int AutocompleteEditViewViews::TextWidth() const {
  // TODO(oshima): add horizontal margin.
  return textfield_->font().GetStringWidth(textfield_->text());
}

bool AutocompleteEditViewViews::IsImeComposing() const {
  return false;
}

views::View* AutocompleteEditViewViews::AddToView(views::View* parent) {
  parent->AddChildView(this);
  AddChildView(textfield_);
  return this;
}

int AutocompleteEditViewViews::OnPerformDrop(
    const views::DropTargetEvent& event) {
  NOTIMPLEMENTED();
  return ui::DragDropTypes::DRAG_NONE;
}

////////////////////////////////////////////////////////////////////////////////
// AutocompleteEditViewViews, NotificationObserver implementation:

void AutocompleteEditViewViews::Observe(NotificationType type,
                                      const NotificationSource& source,
                                      const NotificationDetails& details) {
  DCHECK(type == NotificationType::BROWSER_THEME_CHANGED);
  SetBaseColor();
}

////////////////////////////////////////////////////////////////////////////////
// AutocompleteEditViewViews, views::TextfieldController implementation:

void AutocompleteEditViewViews::ContentsChanged(views::Textfield* sender,
                                                const string16& new_contents) {
}

bool AutocompleteEditViewViews::HandleKeyEvent(
    views::Textfield* textfield,
    const views::KeyEvent& event) {
  delete_at_end_pressed_ = false;

  if (event.key_code() == ui::VKEY_BACK) {
    // Checks if it's currently in keyword search mode.
    if (model_->is_keyword_hint() || model_->keyword().empty())
      return false;
    // If there is selection, let textfield handle the backspace.
    if (textfield_->HasSelection())
      return false;
    // If not at the begining of the text, let textfield handle the backspace.
    if (textfield_->GetCursorPosition())
      return false;
    model_->ClearKeyword(GetText());
    return true;
  }

  if (event.key_code() == ui::VKEY_DELETE && !event.IsAltDown()) {
    delete_at_end_pressed_ =
        (!textfield_->HasSelection() &&
         textfield_->GetCursorPosition() == textfield_->text().length());
  }

  return false;
}

void AutocompleteEditViewViews::OnBeforeUserAction(views::Textfield* sender) {
  OnBeforePossibleChange();
}

void AutocompleteEditViewViews::OnAfterUserAction(views::Textfield* sender) {
  OnAfterPossibleChange();
}

////////////////////////////////////////////////////////////////////////////////
// AutocompleteEditViewViews, private:

size_t AutocompleteEditViewViews::GetTextLength() const {
  // TODO(oshima): Support instant, IME.
  return textfield_->text().length();
}

void AutocompleteEditViewViews::EmphasizeURLComponents() {
  // TODO(oshima): Update URL visual style
  NOTIMPLEMENTED();
}

void AutocompleteEditViewViews::TextChanged() {
  EmphasizeURLComponents();
  model_->OnChanged();
}

void AutocompleteEditViewViews::SetTextAndSelectedRange(
    const string16& text,
    const ui::Range& range) {
  if (text != GetText())
    textfield_->SetText(text);
  textfield_->SelectRange(range);
}

string16 AutocompleteEditViewViews::GetSelectedText() const {
  // TODO(oshima): Support instant, IME.
  return textfield_->GetSelectedText();
}

void AutocompleteEditViewViews::SelectRange(size_t caret, size_t end) {
  const ui::Range range(caret, end);
  textfield_->SelectRange(range);
}

AutocompletePopupView* AutocompleteEditViewViews::CreatePopupView(
    Profile* profile,
    const View* location_bar) {
#if defined(TOUCH_UI)
  return new TouchAutocompletePopupContentsView(
#else
  return new AutocompletePopupContentsView(
#endif
      gfx::Font(), this, model_.get(), profile, location_bar);
}