blob: 55d49387679a0b4f965050c6b3efcff6faabb22e [file] [log] [blame]
// 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);
}