// 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); }