diff options
Diffstat (limited to 'ui/views')
26 files changed, 8867 insertions, 9 deletions
diff --git a/ui/views/controls/textfield/gtk_views_entry.cc b/ui/views/controls/textfield/gtk_views_entry.cc new file mode 100644 index 0000000..d26e370 --- /dev/null +++ b/ui/views/controls/textfield/gtk_views_entry.cc @@ -0,0 +1,74 @@ +// 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 "ui/views/controls/textfield/gtk_views_entry.h" + +#include "base/utf_string_conversions.h" +#include "ui/gfx/canvas_skia_paint.h" +#include "ui/gfx/insets.h" +#include "ui/gfx/skia_utils_gtk.h" +#include "ui/views/controls/textfield/native_textfield_gtk.h" +#include "ui/views/controls/textfield/textfield.h" + +G_BEGIN_DECLS + +G_DEFINE_TYPE(GtkViewsEntry, gtk_views_entry, GTK_TYPE_ENTRY) + +static gint gtk_views_entry_expose_event(GtkWidget *widget, + GdkEventExpose *event) { + views::NativeTextfieldGtk* host = GTK_VIEWS_ENTRY(widget)->host; +#if defined(OS_CHROMEOS) + // Draw textfield background over the default white rectangle. + if (event->window == widget->window) { + gfx::CanvasSkiaPaint canvas(event); + if (!canvas.is_empty() && host) { + host->textfield()->OnPaintBackground(&canvas); + } + } +#endif + + gint result = GTK_WIDGET_CLASS(gtk_views_entry_parent_class)->expose_event( + widget, event); + + GtkEntry* entry = GTK_ENTRY(widget); + + // Internally GtkEntry creates an additional window (text_area) that the + // text is drawn to. We only need paint after that window has painted. + if (host && event->window == entry->text_area && + !host->textfield()->text_to_display_when_empty().empty() && + g_utf8_strlen(gtk_entry_get_text(entry), -1) == 0) { + gfx::CanvasSkiaPaint canvas(event); + if (!canvas.is_empty()) { + gfx::Insets insets = + views::NativeTextfieldGtk::GetEntryInnerBorder(entry); + gfx::Font font = host->textfield()->font(); + const string16 text = host->textfield()->text_to_display_when_empty(); + canvas.DrawStringInt( + text, font, + gfx::GdkColorToSkColor(widget->style->text[GTK_STATE_INSENSITIVE]), + insets.left(), insets.top(), + widget->allocation.width - insets.width(), font.GetHeight()); + } + } + + return result; +} + +static void gtk_views_entry_class_init(GtkViewsEntryClass* views_entry_class) { + GtkWidgetClass* widget_class = + reinterpret_cast<GtkWidgetClass*>(views_entry_class); + widget_class->expose_event = gtk_views_entry_expose_event; +} + +static void gtk_views_entry_init(GtkViewsEntry* entry) { + entry->host = NULL; +} + +GtkWidget* gtk_views_entry_new(views::NativeTextfieldGtk* host) { + gpointer entry = g_object_new(GTK_TYPE_VIEWS_ENTRY, NULL); + GTK_VIEWS_ENTRY(entry)->host = host; + return GTK_WIDGET(entry); +} + +G_END_DECLS diff --git a/ui/views/controls/textfield/gtk_views_entry.h b/ui/views/controls/textfield/gtk_views_entry.h new file mode 100644 index 0000000..1584165 --- /dev/null +++ b/ui/views/controls/textfield/gtk_views_entry.h @@ -0,0 +1,53 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_TEXTFIELD_GTK_VIEWS_ENTRY_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_GTK_VIEWS_ENTRY_H_ +#pragma once + +#include <gdk/gdk.h> +#include <gtk/gtkentry.h> + +namespace views { +class NativeTextfieldGtk; +} + +// GtkViewsEntry is a subclass of GtkEntry that can draw text when the text in +// the entry is empty. For example, this could show the text 'password' in a +// password field when the text is empty. GtkViewsEntry is used internally by +// NativeTextfieldGtk. + +G_BEGIN_DECLS + +#define GTK_TYPE_VIEWS_ENTRY (gtk_views_entry_get_type ()) +#define GTK_VIEWS_ENTRY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_VIEWS_ENTRY, GtkViewsEntry)) +#define GTK_VIEWS_ENTRY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_VIEWS_ENTRY, GtkViewsEntryClass)) +#define GTK_IS_VIEWS_ENTRY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_VIEWS_ENTRY)) +#define GTK_IS_VIEWS_ENTRY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_VIEWS_ENTRY)) +#define GTK_VIEWS_ENTRY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_VIEWS_ENTRY, GtkViewsEntry)) + +typedef struct _GtkViewsEntry GtkViewsEntry; +typedef struct _GtkViewsEntryClass GtkViewsEntryClass; + +struct _GtkViewsEntry { + GtkEntry entry; + views::NativeTextfieldGtk* host; +}; + +struct _GtkViewsEntryClass { + GtkEntryClass parent_class; +}; + +GtkWidget* gtk_views_entry_new(views::NativeTextfieldGtk* host); + +GType gtk_views_entry_get_type(); + +G_END_DECLS + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_GTK_VIEWS_ENTRY_H_ diff --git a/ui/views/controls/textfield/gtk_views_textview.cc b/ui/views/controls/textfield/gtk_views_textview.cc new file mode 100644 index 0000000..8e4ca38 --- /dev/null +++ b/ui/views/controls/textfield/gtk_views_textview.cc @@ -0,0 +1,101 @@ +// 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 "ui/views/controls/textfield/gtk_views_textview.h" + +#include "base/utf_string_conversions.h" +#include "ui/gfx/canvas_skia_paint.h" +#include "ui/gfx/insets.h" +#include "ui/gfx/skia_utils_gtk.h" +#include "ui/views/controls/textfield/native_textfield_gtk.h" +#include "ui/views/controls/textfield/textfield.h" + +G_BEGIN_DECLS + +G_DEFINE_TYPE(GtkViewsTextView, gtk_views_textview, GTK_TYPE_TEXT_VIEW) + +static gint gtk_views_textview_expose_event(GtkWidget *widget, + GdkEventExpose *event) { + GtkTextView* text_view = GTK_TEXT_VIEW(widget); + GdkWindow* text_window = gtk_text_view_get_window(text_view, + GTK_TEXT_WINDOW_TEXT); + if (event->window == text_window) { + gint width, height; + gdk_drawable_get_size (text_window, &width, &height); + gtk_paint_flat_box(widget->style, text_window, + static_cast<GtkStateType>(GTK_WIDGET_STATE(widget)), GTK_SHADOW_NONE, + &event->area, widget, "textview", + 0, 0, width, height); + } + + gint result = GTK_WIDGET_CLASS(gtk_views_textview_parent_class)->expose_event( + widget, event); + + GtkTextBuffer* text_buffer = gtk_text_view_get_buffer(text_view); + views::NativeTextfieldGtk* host = GTK_VIEWS_TEXTVIEW(widget)->host; + + GtkTextIter start; + GtkTextIter end; + gtk_text_buffer_get_bounds(text_buffer, &start, &end); + + // Paint the info text to text window if GtkTextView contains no text. + if (host && event->window == text_window && + !host->textfield()->text_to_display_when_empty().empty() && + gtk_text_iter_equal(&start, &end)) { + gfx::CanvasSkiaPaint canvas(event); + if (!canvas.is_empty()) { + gfx::Insets insets = + views::NativeTextfieldGtk::GetTextViewInnerBorder(text_view); + gfx::Font font = host->textfield()->font(); + const string16 text = host->textfield()->text_to_display_when_empty(); + canvas.DrawStringInt( + text, font, + gfx::GdkColorToSkColor(widget->style->text[GTK_STATE_INSENSITIVE]), + insets.left(), insets.top(), + widget->allocation.width - insets.width(), font.GetHeight()); + } + } + + // Draw border and focus. + if (event->window == widget->window) { + gint width; + gint height; + gdk_drawable_get_size (widget->window, &width, &height); + + bool has_focus = GTK_WIDGET_HAS_FOCUS(widget); + + gtk_paint_shadow(widget->style, widget->window, + has_focus ? GTK_STATE_ACTIVE : GTK_STATE_NORMAL, + GTK_SHADOW_OUT, + &event->area, widget, "textview", + 0, 0, width, height); + if (has_focus) { + gtk_paint_focus(widget->style, widget->window, + static_cast<GtkStateType>(GTK_WIDGET_STATE(widget)), + &event->area, widget, "textview", + 0, 0, width, height); + } + } + + return result; +} + +static void gtk_views_textview_class_init( + GtkViewsTextViewClass* views_textview_class) { + GtkWidgetClass* widget_class = + reinterpret_cast<GtkWidgetClass*>(views_textview_class); + widget_class->expose_event = gtk_views_textview_expose_event; +} + +static void gtk_views_textview_init(GtkViewsTextView* text_view) { + text_view->host = NULL; +} + +GtkWidget* gtk_views_textview_new(views::NativeTextfieldGtk* host) { + gpointer text_view = g_object_new(GTK_TYPE_VIEWS_TEXTVIEW, NULL); + GTK_VIEWS_TEXTVIEW(text_view)->host = host; + return GTK_WIDGET(text_view); +} + +G_END_DECLS diff --git a/ui/views/controls/textfield/gtk_views_textview.h b/ui/views/controls/textfield/gtk_views_textview.h new file mode 100644 index 0000000..9c2e4e6 --- /dev/null +++ b/ui/views/controls/textfield/gtk_views_textview.h @@ -0,0 +1,54 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_TEXTFIELD_GTK_VIEWS_TEXTVIEW_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_GTK_VIEWS_TEXTVIEW_H_ +#pragma once + +#include <gdk/gdk.h> +#include <gtk/gtktextview.h> + +namespace views { +class NativeTextfieldGtk; +} + +// Similar to GtkViewsEntry, GtkViewsTextView is a subclass of GtkTextView +// with a border and ability to show a custom info when text view has no text. + +G_BEGIN_DECLS + +#define GTK_TYPE_VIEWS_TEXTVIEW (gtk_views_textview_get_type()) +#define GTK_VIEWS_TEXTVIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_VIEWS_TEXTVIEW, \ + GtkViewsTextView)) +#define GTK_VIEWS_TEXTVIEW_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_VIEWS_TEXTVIEW, \ + GtkViewsTextViewClass)) +#define GTK_IS_VIEWS_TEXTVIEW(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_VIEWS_TEXTVIEW)) +#define GTK_IS_VIEWS_TEXTVIEW_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_VIEWS_TEXTVIEW)) +#define GTK_VIEWS_TEXTVIEW_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_VIEWS_TEXTVIEW, \ + GtkViewsTextView)) + +typedef struct _GtkViewsTextView GtkViewsTextView; +typedef struct _GtkViewsTextViewClass GtkViewsTextViewClass; + +struct _GtkViewsTextView { + GtkTextView text_view; + views::NativeTextfieldGtk* host; +}; + +struct _GtkViewsTextViewClass { + GtkTextViewClass parent_class; +}; + +GtkWidget* gtk_views_textview_new(views::NativeTextfieldGtk* host); + +GType gtk_views_textview_get_type(); + +G_END_DECLS + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_GTK_VIEWS_TEXTVIEW_H_ diff --git a/ui/views/controls/textfield/native_textfield_gtk.cc b/ui/views/controls/textfield/native_textfield_gtk.cc new file mode 100644 index 0000000..3bab2a1 --- /dev/null +++ b/ui/views/controls/textfield/native_textfield_gtk.cc @@ -0,0 +1,438 @@ +// 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 <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> + +#include "ui/views/controls/textfield/native_textfield_gtk.h" + +#include "base/logging.h" +#include "base/utf_string_conversions.h" +#include "ui/base/range/range.h" +#include "ui/gfx/gtk_util.h" +#include "ui/gfx/insets.h" +#include "ui/gfx/selection_model.h" +#include "ui/gfx/skia_utils_gtk.h" +#include "ui/views/controls/textfield/gtk_views_entry.h" +#include "ui/views/controls/textfield/gtk_views_textview.h" +#include "ui/views/controls/textfield/native_textfield_views.h" +#include "ui/views/controls/textfield/textfield.h" +#include "ui/views/controls/textfield/textfield_controller.h" +#include "ui/views/widget/native_widget_gtk.h" + +namespace views { + +// A character used to hide a text in password mode. +static const char kPasswordChar = '*'; + +// Border width for GtkTextView. +const int kTextViewBorderWidth = 4; + +//////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldGtk, public: + +NativeTextfieldGtk::NativeTextfieldGtk(Textfield* textfield) + : textfield_(textfield), + paste_clipboard_requested_(false) { + // Make |textfield| the focused view, so that when we get focused the focus + // manager sees |textfield| as the focused view (since we are just a wrapper + // view). + set_focus_view(textfield); +} + +NativeTextfieldGtk::~NativeTextfieldGtk() { +} + +// Returns the inner border of an entry. +// static +gfx::Insets NativeTextfieldGtk::GetEntryInnerBorder(GtkEntry* entry) { + const GtkBorder* inner_border = gtk_entry_get_inner_border(entry); + if (inner_border) + return gfx::Insets(*inner_border); + + // No explicit border set, try the style. + GtkBorder* style_border; + gtk_widget_style_get(GTK_WIDGET(entry), "inner-border", &style_border, NULL); + if (style_border) { + gfx::Insets insets = gfx::Insets(*style_border); + gtk_border_free(style_border); + return insets; + } + + // If border is null, Gtk uses 2 on all sides. + return gfx::Insets(2, 2, 2, 2); +} + +gfx::Insets NativeTextfieldGtk::GetTextViewInnerBorder(GtkTextView* text_view) { + return gfx::Insets(kTextViewBorderWidth / 2, kTextViewBorderWidth / 2, + kTextViewBorderWidth / 2, kTextViewBorderWidth / 2); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldGtk, NativeTextfieldWrapper implementation: + +string16 NativeTextfieldGtk::GetText() const { + return UTF8ToUTF16(gtk_entry_get_text(GTK_ENTRY(native_view()))); +} + +void NativeTextfieldGtk::UpdateText() { + if (!native_view()) + return; + gtk_entry_set_text(GTK_ENTRY(native_view()), + UTF16ToUTF8(textfield_->text()).c_str()); +} + +void NativeTextfieldGtk::AppendText(const string16& text) { + if (!native_view()) + return; + gtk_entry_append_text(GTK_ENTRY(native_view()), UTF16ToUTF8(text).c_str()); +} + +string16 NativeTextfieldGtk::GetSelectedText() const { + if (!native_view()) + return string16(); + + string16 result; + + gint start_pos; + gint end_pos; + if (!gtk_editable_get_selection_bounds(GTK_EDITABLE(native_view()), + &start_pos, &end_pos)) + return result; // No selection. + + UTF8ToUTF16(gtk_editable_get_chars(GTK_EDITABLE(native_view()), + start_pos, end_pos), + end_pos - start_pos, &result); + + return result; +} + +void NativeTextfieldGtk::SelectAll() { + if (!native_view()) + return; + // -1 as the end position selects to the end of the text. + gtk_editable_select_region(GTK_EDITABLE(native_view()), 0, -1); +} + +void NativeTextfieldGtk::ClearSelection() { + if (!native_view()) + return; + gtk_editable_select_region(GTK_EDITABLE(native_view()), 0, 0); +} + +void NativeTextfieldGtk::UpdateBorder() { + if (!native_view()) + return; + + if (!textfield_->draw_border()) + gtk_entry_set_has_frame(GTK_ENTRY(native_view()), false); +} + +void NativeTextfieldGtk::UpdateTextColor() { + if (textfield_->use_default_text_color()) { + // Passing NULL as the color undoes the effect of previous calls to + // gtk_widget_modify_text. + gtk_widget_modify_text(native_view(), GTK_STATE_NORMAL, NULL); + return; + } + GdkColor gdk_color = gfx::SkColorToGdkColor(textfield_->text_color()); + gtk_widget_modify_text(native_view(), GTK_STATE_NORMAL, &gdk_color); +} + +void NativeTextfieldGtk::UpdateBackgroundColor() { + if (textfield_->use_default_background_color()) { + // Passing NULL as the color undoes the effect of previous calls to + // gtk_widget_modify_base. + gtk_widget_modify_base(native_view(), GTK_STATE_NORMAL, NULL); + return; + } + GdkColor gdk_color = gfx::SkColorToGdkColor(textfield_->background_color()); + gtk_widget_modify_base(native_view(), GTK_STATE_NORMAL, &gdk_color); +} + +void NativeTextfieldGtk::UpdateReadOnly() { + if (!native_view()) + return; + gtk_editable_set_editable(GTK_EDITABLE(native_view()), + !textfield_->read_only()); +} + +void NativeTextfieldGtk::UpdateFont() { + if (!native_view()) + return; + PangoFontDescription* pfd = textfield_->font().GetNativeFont(); + gtk_widget_modify_font(native_view(), pfd); + pango_font_description_free(pfd); +} + +void NativeTextfieldGtk::UpdateIsPassword() { + if (!native_view()) + return; + gtk_entry_set_visibility(GTK_ENTRY(native_view()), !textfield_->IsPassword()); +} + +void NativeTextfieldGtk::UpdateEnabled() { + if (!native_view()) + return; + SetEnabled(textfield_->IsEnabled()); +} + +gfx::Insets NativeTextfieldGtk::CalculateInsets() { + if (!native_view()) + return gfx::Insets(); + + GtkWidget* widget = native_view(); + gfx::Insets insets; + + GtkEntry* entry = GTK_ENTRY(widget); + insets += GetEntryInnerBorder(entry); + if (entry->has_frame) { + insets += gfx::Insets(widget->style->ythickness, + widget->style->xthickness, + widget->style->ythickness, + widget->style->xthickness); + } + + gboolean interior_focus; + gint focus_width; + gtk_widget_style_get(widget, + "focus-line-width", &focus_width, + "interior-focus", &interior_focus, + NULL); + if (!interior_focus) + insets += gfx::Insets(focus_width, focus_width, focus_width, focus_width); + + return insets; +} + +void NativeTextfieldGtk::UpdateHorizontalMargins() { + if (!native_view()) + return; + + int left, right; + if (!textfield_->GetHorizontalMargins(&left, &right)) + return; + + gfx::Insets insets = GetEntryInnerBorder(GTK_ENTRY(native_view())); + GtkBorder border = {left, right, insets.top(), insets.bottom()}; + gtk_entry_set_inner_border(GTK_ENTRY(native_view()), &border); +} + +void NativeTextfieldGtk::UpdateVerticalMargins() { + if (!native_view()) + return; + + int top, bottom; + if (!textfield_->GetVerticalMargins(&top, &bottom)) + return; + + gfx::Insets insets = GetEntryInnerBorder(GTK_ENTRY(native_view())); + GtkBorder border = {insets.left(), insets.right(), top, bottom}; + gtk_entry_set_inner_border(GTK_ENTRY(native_view()), &border); +} + +bool NativeTextfieldGtk::SetFocus() { + OnFocus(); + return true; +} + +View* NativeTextfieldGtk::GetView() { + return this; +} + +gfx::NativeView NativeTextfieldGtk::GetTestingHandle() const { + return native_view(); +} + +bool NativeTextfieldGtk::IsIMEComposing() const { + return false; +} + +void NativeTextfieldGtk::GetSelectedRange(ui::Range* range) const { + gint start_pos; + gint end_pos; + gtk_editable_get_selection_bounds( + GTK_EDITABLE(native_view()), &start_pos, &end_pos); + *range = ui::Range(start_pos, end_pos); +} + +void NativeTextfieldGtk::SelectRange(const ui::Range& range) { + NOTREACHED(); +} + +void NativeTextfieldGtk::GetSelectionModel(gfx::SelectionModel* sel) const { + NOTREACHED(); +} + +void NativeTextfieldGtk::SelectSelectionModel(const gfx::SelectionModel& sel) { + NOTREACHED(); +} + +size_t NativeTextfieldGtk::GetCursorPosition() const { + NOTREACHED(); + return 0U; +} + +bool NativeTextfieldGtk::HandleKeyPressed(const views::KeyEvent& e) { + return false; +} + +bool NativeTextfieldGtk::HandleKeyReleased(const views::KeyEvent& e) { + return false; +} + +void NativeTextfieldGtk::HandleFocus() { +} + +void NativeTextfieldGtk::HandleBlur() { +} + +ui::TextInputClient* NativeTextfieldGtk::GetTextInputClient() { + return NULL; +} + +void NativeTextfieldGtk::ApplyStyleRange(const gfx::StyleRange& style) { + NOTREACHED(); +} + +void NativeTextfieldGtk::ApplyDefaultStyle() { + NOTREACHED(); +} + +void NativeTextfieldGtk::ClearEditHistory() { + NOTREACHED(); +} + +void NativeTextfieldGtk::OnActivate(GtkWidget* native_widget) { + GdkEvent* event = gtk_get_current_event(); + if (!event || event->type != GDK_KEY_PRESS) + return; + + KeyEvent views_key_event(event); + gboolean handled = false; + + TextfieldController* controller = textfield_->GetController(); + if (controller) + handled = controller->HandleKeyEvent(textfield_, views_key_event); + + Widget* widget = GetWidget(); + if (!handled && widget) { + NativeWidgetGtk* native_widget = + static_cast<NativeWidgetGtk*>(widget->native_widget()); + handled = native_widget->HandleKeyboardEvent(views_key_event); + } + + // Stop signal emission if the key event is handled by us. + if (handled) { + // Only GtkEntry has "activate" signal. + static guint signal_id = g_signal_lookup("activate", GTK_TYPE_ENTRY); + g_signal_stop_emission(native_widget, signal_id, 0); + } +} + +void NativeTextfieldGtk::OnChanged(GObject* object) { + // We need to call TextfieldController::ContentsChanged() explicitly if the + // paste action didn't change the content at all. See http://crbug.com/79002 + const bool call_contents_changed = + paste_clipboard_requested_ && GetText() == textfield_->text(); + textfield_->SyncText(); + textfield_->GetWidget()->NotifyAccessibilityEvent( + textfield_, ui::AccessibilityTypes::EVENT_TEXT_CHANGED, true); + if (call_contents_changed) { + TextfieldController* controller = textfield_->GetController(); + if (controller) + controller->ContentsChanged(textfield_, textfield_->text()); + } + paste_clipboard_requested_ = false; +} + +gboolean NativeTextfieldGtk::OnButtonPressEvent(GtkWidget* widget, + GdkEventButton* event) { + paste_clipboard_requested_ = false; + return false; +} + +gboolean NativeTextfieldGtk::OnButtonReleaseEventAfter(GtkWidget* widget, + GdkEventButton* event) { + textfield_->GetWidget()->NotifyAccessibilityEvent( + textfield_, ui::AccessibilityTypes::EVENT_TEXT_CHANGED, true); + return false; +} + +gboolean NativeTextfieldGtk::OnKeyPressEvent(GtkWidget* widget, + GdkEventKey* event) { + paste_clipboard_requested_ = false; + return false; +} + +gboolean NativeTextfieldGtk::OnKeyPressEventAfter(GtkWidget* widget, + GdkEventKey* event) { + TextfieldController* controller = textfield_->GetController(); + if (controller) { + KeyEvent key_event(reinterpret_cast<GdkEvent*>(event)); + return controller->HandleKeyEvent(textfield_, key_event); + } + return false; +} + +void NativeTextfieldGtk::OnMoveCursor(GtkWidget* widget, + GtkMovementStep step, + gint count, + gboolean extend_selection) { + textfield_->GetWidget()->NotifyAccessibilityEvent( + textfield_, ui::AccessibilityTypes::EVENT_TEXT_CHANGED, true); +} + +void NativeTextfieldGtk::OnPasteClipboard(GtkWidget* widget) { + if (!textfield_->read_only()) + paste_clipboard_requested_ = true; +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldGtk, NativeControlGtk overrides: + +void NativeTextfieldGtk::CreateNativeControl() { + NativeControlCreated(gtk_views_entry_new(this)); + gtk_entry_set_invisible_char(GTK_ENTRY(native_view()), + static_cast<gunichar>(kPasswordChar)); + textfield_->UpdateAllProperties(); +} + +void NativeTextfieldGtk::NativeControlCreated(GtkWidget* widget) { + NativeControlGtk::NativeControlCreated(widget); + + g_signal_connect(widget, "changed", G_CALLBACK(OnChangedThunk), this); + // In order to properly trigger Accelerators bound to VKEY_RETURN, we need + // to send an event when the widget gets the activate signal. + g_signal_connect(widget, "activate", G_CALLBACK(OnActivateThunk), this); + g_signal_connect(widget, "move-cursor", G_CALLBACK(OnMoveCursorThunk), this); + g_signal_connect(widget, "button-press-event", + G_CALLBACK(OnButtonPressEventThunk), this); + g_signal_connect(widget, "key-press-event", + G_CALLBACK(OnKeyPressEventThunk), this); + g_signal_connect(widget, "paste-clipboard", + G_CALLBACK(OnPasteClipboardThunk), this); + + g_signal_connect_after(widget, "button-release-event", + G_CALLBACK(OnButtonReleaseEventAfterThunk), this); + g_signal_connect_after(widget, "key-press-event", + G_CALLBACK(OnKeyPressEventAfterThunk), this); +} + +bool NativeTextfieldGtk::IsPassword() { + return textfield_->IsPassword(); +} + +/////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldWrapper: + +// static +NativeTextfieldWrapper* NativeTextfieldWrapper::CreateWrapper( + Textfield* field) { + if (Widget::IsPureViews()) + return new NativeTextfieldViews(field); + return new NativeTextfieldGtk(field); +} + +} // namespace views diff --git a/ui/views/controls/textfield/native_textfield_gtk.h b/ui/views/controls/textfield/native_textfield_gtk.h new file mode 100644 index 0000000..ea3151c --- /dev/null +++ b/ui/views/controls/textfield/native_textfield_gtk.h @@ -0,0 +1,105 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_GTK_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_GTK_H_ +#pragma once + +#include <gtk/gtk.h> + +#include "base/string16.h" +#include "ui/base/gtk/gtk_signal.h" +#include "ui/views/controls/textfield/native_textfield_wrapper.h" +#include "views/controls/native_control_gtk.h" + +namespace gfx { +class SelectionModel; +} + +namespace views { + +class NativeTextfieldGtk : public NativeControlGtk, + public NativeTextfieldWrapper { + public: + explicit NativeTextfieldGtk(Textfield* parent); + virtual ~NativeTextfieldGtk(); + + // Returns the textfield this NativeTextfieldGtk wraps. + Textfield* textfield() const { return textfield_; } + + // Returns the inner border of the entry. + static gfx::Insets GetEntryInnerBorder(GtkEntry* entry); + static gfx::Insets GetTextViewInnerBorder(GtkTextView* text_view); + + // Overridden from NativeTextfieldWrapper: + virtual string16 GetText() const OVERRIDE; + virtual void UpdateText() OVERRIDE; + virtual void AppendText(const string16& text) OVERRIDE; + virtual string16 GetSelectedText() const OVERRIDE; + virtual void SelectAll() OVERRIDE; + virtual void ClearSelection() OVERRIDE; + virtual void UpdateBorder() OVERRIDE; + virtual void UpdateTextColor() OVERRIDE; + virtual void UpdateBackgroundColor() OVERRIDE; + virtual void UpdateReadOnly() OVERRIDE; + virtual void UpdateFont() OVERRIDE; + virtual void UpdateIsPassword() OVERRIDE; + virtual void UpdateEnabled() OVERRIDE; + virtual gfx::Insets CalculateInsets() OVERRIDE; + virtual void UpdateHorizontalMargins() OVERRIDE; + virtual void UpdateVerticalMargins() OVERRIDE; + virtual bool SetFocus() OVERRIDE; + virtual View* GetView() OVERRIDE; + virtual gfx::NativeView GetTestingHandle() const OVERRIDE; + virtual bool IsIMEComposing() const OVERRIDE; + virtual void GetSelectedRange(ui::Range* range) const OVERRIDE; + virtual void SelectRange(const ui::Range& range) OVERRIDE; + virtual void GetSelectionModel(gfx::SelectionModel* sel) const OVERRIDE; + virtual void SelectSelectionModel(const gfx::SelectionModel& sel) OVERRIDE; + virtual size_t GetCursorPosition() const OVERRIDE; + virtual bool HandleKeyPressed(const views::KeyEvent& e) OVERRIDE; + virtual bool HandleKeyReleased(const views::KeyEvent& e) OVERRIDE; + virtual void HandleFocus() OVERRIDE; + virtual void HandleBlur() OVERRIDE; + virtual ui::TextInputClient* GetTextInputClient() OVERRIDE; + virtual void ApplyStyleRange(const gfx::StyleRange& style) OVERRIDE; + virtual void ApplyDefaultStyle() OVERRIDE; + virtual void ClearEditHistory() OVERRIDE; + + // Overridden from NativeControlGtk: + virtual void CreateNativeControl() OVERRIDE; + virtual void NativeControlCreated(GtkWidget* widget) OVERRIDE; + + // Returns true if the textfield is for password. + bool IsPassword(); + + private: + // Gtk signal callbacks. + CHROMEGTK_CALLBACK_0(NativeTextfieldGtk, void, OnActivate); + CHROMEGTK_CALLBACK_1(NativeTextfieldGtk, gboolean, OnButtonPressEvent, + GdkEventButton*); + CHROMEGTK_CALLBACK_1(NativeTextfieldGtk, gboolean, OnButtonReleaseEventAfter, + GdkEventButton*); + CHROMEG_CALLBACK_0(NativeTextfieldGtk, void, OnChanged, GObject*); + CHROMEGTK_CALLBACK_1(NativeTextfieldGtk, gboolean, OnKeyPressEvent, + GdkEventKey*); + CHROMEGTK_CALLBACK_1(NativeTextfieldGtk, gboolean, OnKeyPressEventAfter, + GdkEventKey*); + CHROMEGTK_CALLBACK_3(NativeTextfieldGtk, void, OnMoveCursor, + GtkMovementStep, gint, gboolean); + CHROMEGTK_CALLBACK_0(NativeTextfieldGtk, void, OnPasteClipboard); + + Textfield* textfield_; + + // Indicates that user requested to paste clipboard. + // The actual paste clipboard action might be performed later if the + // clipboard is not empty. + bool paste_clipboard_requested_; + + DISALLOW_COPY_AND_ASSIGN(NativeTextfieldGtk); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_GTK_H_ diff --git a/ui/views/controls/textfield/native_textfield_views.cc b/ui/views/controls/textfield/native_textfield_views.cc new file mode 100644 index 0000000..8cba8de --- /dev/null +++ b/ui/views/controls/textfield/native_textfield_views.cc @@ -0,0 +1,1085 @@ +// 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 "ui/views/controls/textfield/native_textfield_views.h" + +#include <algorithm> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "base/utf_string_conversions.h" +#include "grit/ui_strings.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/range/range.h" +#include "ui/gfx/canvas.h" +#include "ui/gfx/insets.h" +#include "ui/gfx/render_text.h" +#include "ui/views/controls/textfield/textfield.h" +#include "ui/views/controls/textfield/textfield_controller.h" +#include "ui/views/controls/textfield/textfield_views_model.h" +#include "ui/views/events/event.h" +#include "ui/views/ime/input_method.h" +#include "ui/views/widget/widget.h" +#include "views/background.h" +#include "views/border.h" +#include "views/controls/focusable_border.h" +#include "views/controls/menu/menu_item_view.h" +#include "views/controls/menu/menu_model_adapter.h" +#include "views/controls/menu/menu_runner.h" +#include "views/metrics.h" +#include "views/views_delegate.h" + +#if defined(OS_LINUX) +#include "ui/gfx/gtk_util.h" +#endif + +#if defined(USE_AURA) +#include "ui/aura/cursor.h" +#endif + +namespace { + +// Text color for read only. +const SkColor kReadonlyTextColor = SK_ColorDKGRAY; + +// Parameters to control cursor blinking. +const int kCursorVisibleTimeMs = 800; +const int kCursorInvisibleTimeMs = 500; + +} // namespace + +namespace views { + +const char NativeTextfieldViews::kViewClassName[] = + "views/NativeTextfieldViews"; + +NativeTextfieldViews::NativeTextfieldViews(Textfield* parent) + : textfield_(parent), + ALLOW_THIS_IN_INITIALIZER_LIST(model_(new TextfieldViewsModel(this))), + text_border_(new FocusableBorder()), + is_cursor_visible_(false), + is_drop_cursor_visible_(false), + skip_input_method_cancel_composition_(false), + initiating_drag_(false), + ALLOW_THIS_IN_INITIALIZER_LIST(cursor_timer_(this)), + aggregated_clicks_(0), + last_click_time_(), + last_click_location_(), + ALLOW_THIS_IN_INITIALIZER_LIST(touch_selection_controller_( + TouchSelectionController::create(this))) { + set_border(text_border_); + + // Lowercase is not supported. + DCHECK_NE(parent->style(), Textfield::STYLE_LOWERCASE); + + // Set the default text style. + gfx::StyleRange default_style; + default_style.font = textfield_->font(); + default_style.foreground = textfield_->text_color(); + GetRenderText()->set_default_style(default_style); + GetRenderText()->ApplyDefaultStyle(); + + set_context_menu_controller(this); + set_drag_controller(this); +} + +NativeTextfieldViews::~NativeTextfieldViews() { +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldViews, View overrides: + +bool NativeTextfieldViews::OnMousePressed(const MouseEvent& event) { + OnBeforeUserAction(); + TrackMouseClicks(event); + + // Allow the textfield/omnibox to optionally handle the mouse pressed event. + // This should be removed once native textfield implementations are + // consolidated to textfield. + if (!textfield_->OnMousePressed(event)) + HandleMousePressEvent(event); + + OnAfterUserAction(); + return true; +} + +bool NativeTextfieldViews::OnMouseDragged(const MouseEvent& event) { + // Don't adjust the cursor on a potential drag and drop. + if (initiating_drag_) + return true; + + OnBeforeUserAction(); + if (MoveCursorTo(event.location(), true)) + SchedulePaint(); + OnAfterUserAction(); + return true; +} + +void NativeTextfieldViews::OnMouseReleased(const MouseEvent& event) { + OnBeforeUserAction(); + // Cancel suspected drag initiations, the user was clicking in the selection. + if (initiating_drag_ && MoveCursorTo(event.location(), false)) + SchedulePaint(); + initiating_drag_ = false; + OnAfterUserAction(); +} + +bool NativeTextfieldViews::OnKeyPressed(const KeyEvent& event) { + // OnKeyPressed/OnKeyReleased/OnFocus/OnBlur will never be invoked on + // NativeTextfieldViews as it will never gain focus. + NOTREACHED(); + return false; +} + +bool NativeTextfieldViews::OnKeyReleased(const KeyEvent& event) { + NOTREACHED(); + return false; +} + +bool NativeTextfieldViews::GetDropFormats( + int* formats, + std::set<OSExchangeData::CustomFormat>* custom_formats) { + if (!textfield_->IsEnabled() || textfield_->read_only()) + return false; + // TODO(msw): Can we support URL, FILENAME, etc.? + *formats = ui::OSExchangeData::STRING; + return true; +} + +bool NativeTextfieldViews::CanDrop(const OSExchangeData& data) { + return textfield_->IsEnabled() && !textfield_->read_only() && + data.HasString(); +} + +int NativeTextfieldViews::OnDragUpdated(const DropTargetEvent& event) { + DCHECK(CanDrop(event.data())); + bool in_selection = GetRenderText()->IsPointInSelection(event.location()); + is_drop_cursor_visible_ = !in_selection; + // TODO(msw): Pan over text when the user drags to the visible text edge. + OnCaretBoundsChanged(); + SchedulePaint(); + + if (initiating_drag_) { + if (in_selection) + return ui::DragDropTypes::DRAG_NONE; + return event.IsControlDown() ? ui::DragDropTypes::DRAG_COPY : + ui::DragDropTypes::DRAG_MOVE; + } + return ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE; +} + +int NativeTextfieldViews::OnPerformDrop(const DropTargetEvent& event) { + DCHECK(CanDrop(event.data())); + DCHECK(!initiating_drag_ || + !GetRenderText()->IsPointInSelection(event.location())); + OnBeforeUserAction(); + skip_input_method_cancel_composition_ = true; + + // TODO(msw): Remove final reference to FindCursorPosition. + gfx::SelectionModel drop_destination = + GetRenderText()->FindCursorPosition(event.location()); + string16 text; + event.data().GetString(&text); + + // We'll delete the current selection for a drag and drop within this view. + bool move = initiating_drag_ && !event.IsControlDown() && + event.source_operations() & ui::DragDropTypes::DRAG_MOVE; + if (move) { + gfx::SelectionModel selected; + model_->GetSelectionModel(&selected); + // Adjust the drop destination if it is on or after the current selection. + size_t max_of_selected_range = std::max(selected.selection_start(), + selected.selection_end()); + size_t min_of_selected_range = std::min(selected.selection_start(), + selected.selection_end()); + size_t selected_range_length = max_of_selected_range - + min_of_selected_range; + size_t drop_destination_end = drop_destination.selection_end(); + if (max_of_selected_range <= drop_destination_end) + drop_destination_end -= selected_range_length; + else if (min_of_selected_range <= drop_destination_end) + drop_destination_end = min_of_selected_range; + model_->DeleteSelectionAndInsertTextAt(text, drop_destination_end); + } else { + model_->MoveCursorTo(drop_destination); + // Drop always inserts text even if the textfield is not in insert mode. + model_->InsertText(text); + } + skip_input_method_cancel_composition_ = false; + UpdateAfterChange(true, true); + OnAfterUserAction(); + return move ? ui::DragDropTypes::DRAG_MOVE : ui::DragDropTypes::DRAG_COPY; +} + +void NativeTextfieldViews::OnDragDone() { + initiating_drag_ = false; + is_drop_cursor_visible_ = false; +} + +void NativeTextfieldViews::OnPaint(gfx::Canvas* canvas) { + text_border_->set_has_focus(textfield_->HasFocus()); + OnPaintBackground(canvas); + PaintTextAndCursor(canvas); + if (textfield_->draw_border()) + OnPaintBorder(canvas); +} + +void NativeTextfieldViews::OnFocus() { + NOTREACHED(); +} + +void NativeTextfieldViews::OnBlur() { + NOTREACHED(); +} + +void NativeTextfieldViews::SelectRect(const gfx::Point& start, + const gfx::Point& end) { + if (GetTextInputType() == ui::TEXT_INPUT_TYPE_NONE) + return; + + gfx::SelectionModel start_pos = GetRenderText()->FindCursorPosition(start); + gfx::SelectionModel end_pos = GetRenderText()->FindCursorPosition(end); + + OnBeforeUserAction(); + // Merge selection models of "start_pos" and "end_pos" so that + // selection start is the value from "start_pos", while selection end, + // caret position, and caret placement are values from "end_pos". + if (start_pos.selection_start() == end_pos.selection_end()) + model_->SelectSelectionModel(end_pos); + else + model_->SelectRange(ui::Range(start_pos.selection_start(), + end_pos.selection_end())); + + OnCaretBoundsChanged(); + SchedulePaint(); + OnAfterUserAction(); +} + +gfx::NativeCursor NativeTextfieldViews::GetCursor(const MouseEvent& event) { + bool in_selection = GetRenderText()->IsPointInSelection(event.location()); + bool drag_event = event.type() == ui::ET_MOUSE_DRAGGED; + bool text_cursor = !initiating_drag_ && (drag_event || !in_selection); +#if defined(USE_AURA) + return text_cursor ? aura::kCursorIBeam : aura::kCursorNull; +#elif defined(OS_WIN) + static HCURSOR ibeam = LoadCursor(NULL, IDC_IBEAM); + static HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); + return text_cursor ? ibeam : arrow; +#else + return text_cursor ? gfx::GetCursor(GDK_XTERM) : NULL; +#endif +} + +///////////////////////////////////////////////////////////////// +// NativeTextfieldViews, ContextMenuController overrides: +void NativeTextfieldViews::ShowContextMenuForView(View* source, + const gfx::Point& p, + bool is_mouse_gesture) { + UpdateContextMenu(); + if (context_menu_runner_->RunMenuAt( + GetWidget(), NULL, gfx::Rect(p, gfx::Size()), + views::MenuItemView::TOPLEFT, MenuRunner::HAS_MNEMONICS) == + MenuRunner::MENU_DELETED) + return; +} + +///////////////////////////////////////////////////////////////// +// NativeTextfieldViews, views::DragController overrides: +void NativeTextfieldViews::WriteDragDataForView(views::View* sender, + const gfx::Point& press_pt, + OSExchangeData* data) { + DCHECK_NE(ui::DragDropTypes::DRAG_NONE, + GetDragOperationsForView(sender, press_pt)); + data->SetString(GetSelectedText()); +} + +int NativeTextfieldViews::GetDragOperationsForView(views::View* sender, + const gfx::Point& p) { + if (!textfield_->IsEnabled() || !GetRenderText()->IsPointInSelection(p)) + return ui::DragDropTypes::DRAG_NONE; + if (sender == this && !textfield_->read_only()) + return ui::DragDropTypes::DRAG_MOVE | ui::DragDropTypes::DRAG_COPY; + return ui::DragDropTypes::DRAG_COPY; +} + +bool NativeTextfieldViews::CanStartDragForView(View* sender, + const gfx::Point& press_pt, + const gfx::Point& p) { + return GetRenderText()->IsPointInSelection(press_pt); +} + +///////////////////////////////////////////////////////////////// +// NativeTextfieldViews, NativeTextifieldWrapper overrides: + +string16 NativeTextfieldViews::GetText() const { + return model_->GetText(); +} + +void NativeTextfieldViews::UpdateText() { + model_->SetText(textfield_->text()); + OnCaretBoundsChanged(); + SchedulePaint(); +} + +void NativeTextfieldViews::AppendText(const string16& text) { + if (text.empty()) + return; + model_->Append(text); + OnCaretBoundsChanged(); + SchedulePaint(); +} + +string16 NativeTextfieldViews::GetSelectedText() const { + return model_->GetSelectedText(); +} + +void NativeTextfieldViews::SelectAll() { + OnBeforeUserAction(); + model_->SelectAll(); + OnCaretBoundsChanged(); + SchedulePaint(); + OnAfterUserAction(); +} + +void NativeTextfieldViews::ClearSelection() { + OnBeforeUserAction(); + model_->ClearSelection(); + OnCaretBoundsChanged(); + SchedulePaint(); + OnAfterUserAction(); +} + +void NativeTextfieldViews::UpdateBorder() { + if (textfield_->draw_border()) { + gfx::Insets insets = GetInsets(); + textfield_->SetHorizontalMargins(insets.left(), insets.right()); + textfield_->SetVerticalMargins(insets.top(), insets.bottom()); + } else { + textfield_->SetHorizontalMargins(0, 0); + textfield_->SetVerticalMargins(0, 0); + } +} + +void NativeTextfieldViews::UpdateTextColor() { + SchedulePaint(); +} + +void NativeTextfieldViews::UpdateBackgroundColor() { + // TODO(oshima): Background has to match the border's shape. + set_background( + Background::CreateSolidBackground(textfield_->background_color())); + SchedulePaint(); +} + +void NativeTextfieldViews::UpdateReadOnly() { + // Update the default text style. + gfx::StyleRange default_style(GetRenderText()->default_style()); + default_style.foreground = textfield_->read_only() ? kReadonlyTextColor : + textfield_->text_color(); + GetRenderText()->set_default_style(default_style); + GetRenderText()->ApplyDefaultStyle(); + + SchedulePaint(); + OnTextInputTypeChanged(); +} + +void NativeTextfieldViews::UpdateFont() { + // Update the default text style. + gfx::StyleRange default_style(GetRenderText()->default_style()); + default_style.font = textfield_->font(); + GetRenderText()->set_default_style(default_style); + GetRenderText()->ApplyDefaultStyle(); + + OnCaretBoundsChanged(); +} + +void NativeTextfieldViews::UpdateIsPassword() { + model_->set_is_password(textfield_->IsPassword()); + OnCaretBoundsChanged(); + SchedulePaint(); + OnTextInputTypeChanged(); +} + +void NativeTextfieldViews::UpdateEnabled() { + SetEnabled(textfield_->IsEnabled()); + SchedulePaint(); + OnTextInputTypeChanged(); +} + +gfx::Insets NativeTextfieldViews::CalculateInsets() { + return GetInsets(); +} + +void NativeTextfieldViews::UpdateHorizontalMargins() { + int left, right; + if (!textfield_->GetHorizontalMargins(&left, &right)) + return; + gfx::Insets inset = GetInsets(); + + text_border_->SetInsets(inset.top(), left, inset.bottom(), right); + OnCaretBoundsChanged(); +} + +void NativeTextfieldViews::UpdateVerticalMargins() { + int top, bottom; + if (!textfield_->GetVerticalMargins(&top, &bottom)) + return; + gfx::Insets inset = GetInsets(); + text_border_->SetInsets(top, inset.left(), bottom, inset.right()); + OnCaretBoundsChanged(); +} + +bool NativeTextfieldViews::SetFocus() { + return false; +} + +View* NativeTextfieldViews::GetView() { + return this; +} + +gfx::NativeView NativeTextfieldViews::GetTestingHandle() const { + NOTREACHED(); + return NULL; +} + +bool NativeTextfieldViews::IsIMEComposing() const { + return model_->HasCompositionText(); +} + +void NativeTextfieldViews::GetSelectedRange(ui::Range* range) const { + model_->GetSelectedRange(range); +} + +void NativeTextfieldViews::SelectRange(const ui::Range& range) { + model_->SelectRange(range); + OnCaretBoundsChanged(); + SchedulePaint(); +} + +void NativeTextfieldViews::GetSelectionModel(gfx::SelectionModel* sel) const { + model_->GetSelectionModel(sel); +} + +void NativeTextfieldViews::SelectSelectionModel( + const gfx::SelectionModel& sel) { + model_->SelectSelectionModel(sel); + OnCaretBoundsChanged(); + SchedulePaint(); +} + +size_t NativeTextfieldViews::GetCursorPosition() const { + return model_->GetCursorPosition(); +} + +bool NativeTextfieldViews::HandleKeyPressed(const KeyEvent& e) { + TextfieldController* controller = textfield_->GetController(); + bool handled = false; + if (controller) + handled = controller->HandleKeyEvent(textfield_, e); + return handled || HandleKeyEvent(e); +} + +bool NativeTextfieldViews::HandleKeyReleased(const KeyEvent& e) { + return true; +} + +void NativeTextfieldViews::HandleFocus() { + GetRenderText()->set_focused(true); + is_cursor_visible_ = true; + SchedulePaint(); + OnCaretBoundsChanged(); + // Start blinking cursor. + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&NativeTextfieldViews::UpdateCursor, + cursor_timer_.GetWeakPtr()), + kCursorVisibleTimeMs); +} + +void NativeTextfieldViews::HandleBlur() { + GetRenderText()->set_focused(false); + // Stop blinking cursor. + cursor_timer_.InvalidateWeakPtrs(); + if (is_cursor_visible_) { + is_cursor_visible_ = false; + RepaintCursor(); + } + + if (touch_selection_controller_.get()) + touch_selection_controller_->ClientViewLostFocus(); +} + +ui::TextInputClient* NativeTextfieldViews::GetTextInputClient() { + return textfield_->read_only() ? NULL : this; +} + +void NativeTextfieldViews::ClearEditHistory() { + model_->ClearEditHistory(); +} + +///////////////////////////////////////////////////////////////// +// NativeTextfieldViews, ui::SimpleMenuModel::Delegate overrides: + +bool NativeTextfieldViews::IsCommandIdChecked(int command_id) const { + return true; +} + +bool NativeTextfieldViews::IsCommandIdEnabled(int command_id) const { + bool editable = !textfield_->read_only(); + string16 result; + switch (command_id) { + case IDS_APP_CUT: + return editable && model_->HasSelection(); + case IDS_APP_COPY: + return model_->HasSelection(); + case IDS_APP_PASTE: + ViewsDelegate::views_delegate->GetClipboard() + ->ReadText(ui::Clipboard::BUFFER_STANDARD, &result); + return editable && !result.empty(); + case IDS_APP_DELETE: + return editable && model_->HasSelection(); + case IDS_APP_SELECT_ALL: + return true; + default: + NOTREACHED(); + return false; + } +} + +bool NativeTextfieldViews::GetAcceleratorForCommandId(int command_id, + ui::Accelerator* accelerator) { + return false; +} + +void NativeTextfieldViews::ExecuteCommand(int command_id) { + bool text_changed = false; + bool editable = !textfield_->read_only(); + OnBeforeUserAction(); + switch (command_id) { + case IDS_APP_CUT: + if (editable) + text_changed = model_->Cut(); + break; + case IDS_APP_COPY: + model_->Copy(); + break; + case IDS_APP_PASTE: + if (editable) + text_changed = Paste(); + break; + case IDS_APP_DELETE: + if (editable) + text_changed = model_->Delete(); + break; + case IDS_APP_SELECT_ALL: + SelectAll(); + break; + default: + NOTREACHED() << "unknown command: " << command_id; + break; + } + + // The cursor must have changed if text changed during cut/paste/delete. + UpdateAfterChange(text_changed, text_changed); + OnAfterUserAction(); +} + +void NativeTextfieldViews::ApplyStyleRange(const gfx::StyleRange& style) { + GetRenderText()->ApplyStyleRange(style); + SchedulePaint(); +} + +void NativeTextfieldViews::ApplyDefaultStyle() { + GetRenderText()->ApplyDefaultStyle(); + SchedulePaint(); +} + +void NativeTextfieldViews::OnBoundsChanged(const gfx::Rect& previous_bounds) { + // Set the RenderText display area. + gfx::Insets insets = GetInsets(); + gfx::Rect display_rect(insets.left(), + insets.top(), + width() - insets.width(), + height() - insets.height()); + GetRenderText()->SetDisplayRect(display_rect); + OnCaretBoundsChanged(); +} + +/////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldViews, ui::TextInputClient implementation, private: + +void NativeTextfieldViews::SetCompositionText( + const ui::CompositionText& composition) { + if (GetTextInputType() == ui::TEXT_INPUT_TYPE_NONE) + return; + + OnBeforeUserAction(); + skip_input_method_cancel_composition_ = true; + model_->SetCompositionText(composition); + skip_input_method_cancel_composition_ = false; + UpdateAfterChange(true, true); + OnAfterUserAction(); +} + +void NativeTextfieldViews::ConfirmCompositionText() { + if (!model_->HasCompositionText()) + return; + + OnBeforeUserAction(); + skip_input_method_cancel_composition_ = true; + model_->ConfirmCompositionText(); + skip_input_method_cancel_composition_ = false; + UpdateAfterChange(true, true); + OnAfterUserAction(); +} + +void NativeTextfieldViews::ClearCompositionText() { + if (!model_->HasCompositionText()) + return; + + OnBeforeUserAction(); + skip_input_method_cancel_composition_ = true; + model_->CancelCompositionText(); + skip_input_method_cancel_composition_ = false; + UpdateAfterChange(true, true); + OnAfterUserAction(); +} + +void NativeTextfieldViews::InsertText(const string16& text) { + // TODO(suzhe): Filter invalid characters. + if (GetTextInputType() == ui::TEXT_INPUT_TYPE_NONE || text.empty()) + return; + + OnBeforeUserAction(); + skip_input_method_cancel_composition_ = true; + if (GetRenderText()->insert_mode()) + model_->InsertText(text); + else + model_->ReplaceText(text); + skip_input_method_cancel_composition_ = false; + UpdateAfterChange(true, true); + OnAfterUserAction(); +} + +void NativeTextfieldViews::InsertChar(char16 ch, int flags) { + if (GetTextInputType() == ui::TEXT_INPUT_TYPE_NONE || + !ShouldInsertChar(ch, flags)) { + return; + } + + OnBeforeUserAction(); + skip_input_method_cancel_composition_ = true; + if (GetRenderText()->insert_mode()) + model_->InsertChar(ch); + else + model_->ReplaceChar(ch); + skip_input_method_cancel_composition_ = false; + UpdateAfterChange(true, true); + OnAfterUserAction(); +} + +ui::TextInputType NativeTextfieldViews::GetTextInputType() const { + return textfield_->GetTextInputType(); +} + +gfx::Rect NativeTextfieldViews::GetCaretBounds() { + return GetRenderText()->GetUpdatedCursorBounds(); +} + +bool NativeTextfieldViews::HasCompositionText() { + return model_->HasCompositionText(); +} + +bool NativeTextfieldViews::GetTextRange(ui::Range* range) { + // We don't allow the input method to retrieve or delete content from a + // password box. + if (GetTextInputType() != ui::TEXT_INPUT_TYPE_TEXT) + return false; + + model_->GetTextRange(range); + return true; +} + +bool NativeTextfieldViews::GetCompositionTextRange(ui::Range* range) { + if (GetTextInputType() != ui::TEXT_INPUT_TYPE_TEXT) + return false; + + model_->GetCompositionTextRange(range); + return true; +} + +bool NativeTextfieldViews::GetSelectionRange(ui::Range* range) { + if (GetTextInputType() != ui::TEXT_INPUT_TYPE_TEXT) + return false; + + gfx::SelectionModel sel; + model_->GetSelectionModel(&sel); + range->set_start(sel.selection_start()); + range->set_end(sel.selection_end()); + return true; +} + +bool NativeTextfieldViews::SetSelectionRange(const ui::Range& range) { + if (GetTextInputType() != ui::TEXT_INPUT_TYPE_TEXT || !range.IsValid()) + return false; + + OnBeforeUserAction(); + SelectRange(range); + OnAfterUserAction(); + return true; +} + +bool NativeTextfieldViews::DeleteRange(const ui::Range& range) { + if (GetTextInputType() != ui::TEXT_INPUT_TYPE_TEXT || range.is_empty()) + return false; + + OnBeforeUserAction(); + model_->SelectRange(range); + if (model_->HasSelection()) { + model_->DeleteSelection(); + UpdateAfterChange(true, true); + } + OnAfterUserAction(); + return true; +} + +bool NativeTextfieldViews::GetTextFromRange( + const ui::Range& range, + string16* text) { + if (GetTextInputType() != ui::TEXT_INPUT_TYPE_TEXT || !range.IsValid()) + return false; + + ui::Range text_range; + if (!GetTextRange(&text_range) || !text_range.Contains(range)) + return false; + + *text = model_->GetTextFromRange(range); + return true; +} + +void NativeTextfieldViews::OnInputMethodChanged() { + NOTIMPLEMENTED(); +} + +bool NativeTextfieldViews::ChangeTextDirectionAndLayoutAlignment( + base::i18n::TextDirection direction) { + NOTIMPLEMENTED(); + return false; +} + +void NativeTextfieldViews::OnCompositionTextConfirmedOrCleared() { + if (skip_input_method_cancel_composition_) + return; + DCHECK(textfield_->GetInputMethod()); + textfield_->GetInputMethod()->CancelComposition(textfield_); +} + +gfx::RenderText* NativeTextfieldViews::GetRenderText() const { + return model_->render_text(); +} + +void NativeTextfieldViews::UpdateCursor() { + is_cursor_visible_ = !is_cursor_visible_; + RepaintCursor(); + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&NativeTextfieldViews::UpdateCursor, + cursor_timer_.GetWeakPtr()), + is_cursor_visible_ ? kCursorVisibleTimeMs : kCursorInvisibleTimeMs); +} + +void NativeTextfieldViews::RepaintCursor() { + gfx::Rect r(GetCaretBounds()); + r.Inset(-1, -1, -1, -1); + SchedulePaintInRect(r); +} + +void NativeTextfieldViews::PaintTextAndCursor(gfx::Canvas* canvas) { + canvas->Save(); + GetRenderText()->set_cursor_visible(is_drop_cursor_visible_ || + (is_cursor_visible_ && !model_->HasSelection())); + // Draw the text, cursor, and selection. + GetRenderText()->Draw(canvas); + canvas->Restore(); +} + +bool NativeTextfieldViews::HandleKeyEvent(const KeyEvent& key_event) { + // TODO(oshima): Refactor and consolidate with ExecuteCommand. + if (key_event.type() == ui::ET_KEY_PRESSED) { + ui::KeyboardCode key_code = key_event.key_code(); + // TODO(oshima): shift-tab does not work. Figure out why and fix. + if (key_code == ui::VKEY_TAB) + return false; + + OnBeforeUserAction(); + bool editable = !textfield_->read_only(); + bool selection = key_event.IsShiftDown(); + bool control = key_event.IsControlDown(); + bool text_changed = false; + bool cursor_changed = false; + switch (key_code) { + case ui::VKEY_Z: + if (control && editable) + cursor_changed = text_changed = model_->Undo(); + break; + case ui::VKEY_Y: + if (control && editable) + cursor_changed = text_changed = model_->Redo(); + break; + case ui::VKEY_A: + if (control) { + model_->SelectAll(); + cursor_changed = true; + } + break; + case ui::VKEY_X: + if (control && editable) + cursor_changed = text_changed = model_->Cut(); + break; + case ui::VKEY_C: + if (control) + model_->Copy(); + break; + case ui::VKEY_V: + if (control && editable) + cursor_changed = text_changed = Paste(); + break; + case ui::VKEY_RIGHT: + model_->MoveCursorRight( + control ? gfx::WORD_BREAK : gfx::CHARACTER_BREAK, selection); + cursor_changed = true; + break; + case ui::VKEY_LEFT: + model_->MoveCursorLeft( + control ? gfx::WORD_BREAK : gfx::CHARACTER_BREAK, selection); + cursor_changed = true; + break; + case ui::VKEY_END: + case ui::VKEY_HOME: + if ((key_code == ui::VKEY_HOME) == + (GetRenderText()->GetTextDirection() == base::i18n::RIGHT_TO_LEFT)) + model_->MoveCursorRight(gfx::LINE_BREAK, selection); + else + model_->MoveCursorLeft(gfx::LINE_BREAK, selection); + cursor_changed = true; + break; + case ui::VKEY_BACK: + if (!editable) + break; + if (!model_->HasSelection()) { + if (selection && control) { + // If both shift and control are pressed, then erase upto the + // beginning of the buffer in ChromeOS. In windows, do nothing. +#if defined(OS_WIN) + break; +#else + model_->MoveCursorLeft(gfx::LINE_BREAK, true); +#endif + } else if (control) { + // If only control is pressed, then erase the previous word. + model_->MoveCursorLeft(gfx::WORD_BREAK, true); + } + } + text_changed = model_->Backspace(); + cursor_changed = true; + break; + case ui::VKEY_DELETE: + if (!editable) + break; + if (!model_->HasSelection()) { + if (selection && control) { + // If both shift and control are pressed, then erase upto the + // end of the buffer in ChromeOS. In windows, do nothing. +#if defined(OS_WIN) + break; +#else + model_->MoveCursorRight(gfx::LINE_BREAK, true); +#endif + } else if (control) { + // If only control is pressed, then erase the next word. + model_->MoveCursorRight(gfx::WORD_BREAK, true); + } + } + cursor_changed = text_changed = model_->Delete(); + break; + case ui::VKEY_INSERT: + GetRenderText()->ToggleInsertMode(); + cursor_changed = true; + break; + default: + break; + } + + // We must have input method in order to support text input. + DCHECK(textfield_->GetInputMethod()); + + UpdateAfterChange(text_changed, cursor_changed); + OnAfterUserAction(); + return (text_changed || cursor_changed); + } + return false; +} + +bool NativeTextfieldViews::MoveCursorTo(const gfx::Point& point, bool select) { + if (!model_->MoveCursorTo(point, select)) + return false; + OnCaretBoundsChanged(); + return true; +} + +void NativeTextfieldViews::PropagateTextChange() { + textfield_->SyncText(); +} + +void NativeTextfieldViews::UpdateAfterChange(bool text_changed, + bool cursor_changed) { + if (text_changed) + PropagateTextChange(); + if (cursor_changed) { + is_cursor_visible_ = true; + RepaintCursor(); + } + if (text_changed || cursor_changed) { + OnCaretBoundsChanged(); + SchedulePaint(); + } +} + +void NativeTextfieldViews::UpdateContextMenu() { + if (!context_menu_contents_.get()) { + context_menu_contents_.reset(new ui::SimpleMenuModel(this)); + context_menu_contents_->AddItemWithStringId(IDS_APP_CUT, IDS_APP_CUT); + context_menu_contents_->AddItemWithStringId(IDS_APP_COPY, IDS_APP_COPY); + context_menu_contents_->AddItemWithStringId(IDS_APP_PASTE, IDS_APP_PASTE); + context_menu_contents_->AddItemWithStringId(IDS_APP_DELETE, IDS_APP_DELETE); + context_menu_contents_->AddSeparator(); + context_menu_contents_->AddItemWithStringId(IDS_APP_SELECT_ALL, + IDS_APP_SELECT_ALL); + + context_menu_delegate_.reset( + new views::MenuModelAdapter(context_menu_contents_.get())); + context_menu_runner_.reset( + new MenuRunner(new views::MenuItemView(context_menu_delegate_.get()))); + } + + context_menu_delegate_->BuildMenu(context_menu_runner_->GetMenu()); +} + +void NativeTextfieldViews::OnTextInputTypeChanged() { + // TODO(suzhe): changed from DCHECK. See http://crbug.com/81320. + if (textfield_->GetInputMethod()) + textfield_->GetInputMethod()->OnTextInputTypeChanged(textfield_); +} + +void NativeTextfieldViews::OnCaretBoundsChanged() { + // TODO(suzhe): changed from DCHECK. See http://crbug.com/81320. + if (textfield_->GetInputMethod()) + textfield_->GetInputMethod()->OnCaretBoundsChanged(textfield_); + + // Notify selection controller + if (!touch_selection_controller_.get()) + return; + gfx::RenderText* render_text = GetRenderText(); + const gfx::SelectionModel& sel = render_text->selection_model(); + gfx::SelectionModel start_sel = + render_text->GetSelectionModelForSelectionStart(); + gfx::Rect start_cursor = render_text->GetCursorBounds(start_sel, true); + gfx::Rect end_cursor = render_text->GetCursorBounds(sel, true); + gfx::Point start(start_cursor.x(), start_cursor.bottom() - 1); + gfx::Point end(end_cursor.x(), end_cursor.bottom() - 1); + touch_selection_controller_->SelectionChanged(start, end); +} + +void NativeTextfieldViews::OnBeforeUserAction() { + TextfieldController* controller = textfield_->GetController(); + if (controller) + controller->OnBeforeUserAction(textfield_); +} + +void NativeTextfieldViews::OnAfterUserAction() { + TextfieldController* controller = textfield_->GetController(); + if (controller) + controller->OnAfterUserAction(textfield_); +} + +bool NativeTextfieldViews::Paste() { + const bool success = model_->Paste(); + + // Calls TextfieldController::ContentsChanged() explicitly if the paste action + // did not change the content at all. See http://crbug.com/79002 + if (success && GetText() == textfield_->text()) { + TextfieldController* controller = textfield_->GetController(); + if (controller) + controller->ContentsChanged(textfield_, textfield_->text()); + } + return success; +} + +void NativeTextfieldViews::TrackMouseClicks(const MouseEvent& event) { + if (event.IsOnlyLeftMouseButton()) { + base::TimeDelta time_delta = event.time_stamp() - last_click_time_; + gfx::Point location_delta = event.location().Subtract(last_click_location_); + if (time_delta.InMilliseconds() <= GetDoubleClickInterval() && + !ExceededDragThreshold(location_delta.x(), location_delta.y())) { + aggregated_clicks_ = (aggregated_clicks_ + 1) % 3; + } else { + aggregated_clicks_ = 0; + } + last_click_time_ = event.time_stamp(); + last_click_location_ = event.location(); + } +} + +void NativeTextfieldViews::HandleMousePressEvent(const MouseEvent& event) { + if (event.IsOnlyLeftMouseButton()) { + textfield_->RequestFocus(); + + initiating_drag_ = false; + bool can_drag = true; + + switch(aggregated_clicks_) { + case 0: + if (can_drag && GetRenderText()->IsPointInSelection(event.location())) + initiating_drag_ = true; + else + MoveCursorTo(event.location(), event.IsShiftDown()); + break; + case 1: + model_->SelectWord(); + OnCaretBoundsChanged(); + break; + case 2: + model_->SelectAll(); + OnCaretBoundsChanged(); + break; + default: + NOTREACHED(); + } + SchedulePaint(); + } +} + +// static +bool NativeTextfieldViews::ShouldInsertChar(char16 ch, int flags) { + // Filter out all control characters, including tab and new line characters, + // and all characters with Alt modifier. But we need to allow characters with + // AltGr modifier. + // On Windows AltGr is represented by Alt+Ctrl, and on Linux it's a different + // flag that we don't care about. + return ((ch >= 0x20 && ch < 0x7F) || ch > 0x9F) && + (flags & ~(ui::EF_SHIFT_DOWN | ui::EF_CAPS_LOCK_DOWN)) != ui::EF_ALT_DOWN; +} + +#if defined(USE_AURA) +// static +NativeTextfieldWrapper* NativeTextfieldWrapper::CreateWrapper( + Textfield* field) { + return new NativeTextfieldViews(field); +} +#endif + +} // namespace views diff --git a/ui/views/controls/textfield/native_textfield_views.h b/ui/views/controls/textfield/native_textfield_views.h new file mode 100644 index 0000000..2cae9f4 --- /dev/null +++ b/ui/views/controls/textfield/native_textfield_views.h @@ -0,0 +1,267 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_VIEWS_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_VIEWS_H_ +#pragma once + +#include "base/memory/weak_ptr.h" +#include "base/string16.h" +#include "ui/base/ime/text_input_client.h" +#include "ui/base/models/simple_menu_model.h" +#include "ui/gfx/font.h" +#include "ui/views/controls/textfield/native_textfield_wrapper.h" +#include "ui/views/controls/textfield/textfield_views_model.h" +#include "ui/views/touchui/touch_selection_controller.h" +#include "views/border.h" +#include "views/context_menu_controller.h" +#include "views/drag_controller.h" +#include "views/view.h" + +namespace base { +class Time; +} + +namespace gfx { +class Canvas; +} + +namespace views { + +class FocusableBorder; +class KeyEvent; +class MenuModelAdapter; +class MenuRunner; + +// A views/skia only implementation of NativeTextfieldWrapper. +// No platform specific code is used. +// Following features are not yet supported. +// * BIDI/Complex script. +// * Support surrogate pair, or maybe we should just use UTF32 internally. +// * X selection (only if we want to support). +// Once completed, this will replace Textfield, NativeTextfieldWin and +// NativeTextfieldGtk. +class VIEWS_EXPORT NativeTextfieldViews : public TouchSelectionClientView, + public ContextMenuController, + public DragController, + public NativeTextfieldWrapper, + public ui::TextInputClient, + public TextfieldViewsModel::Delegate { + public: + explicit NativeTextfieldViews(Textfield* parent); + virtual ~NativeTextfieldViews(); + + // View overrides: + virtual gfx::NativeCursor GetCursor(const MouseEvent& event) OVERRIDE; + virtual bool OnMousePressed(const MouseEvent& event) OVERRIDE; + virtual bool OnMouseDragged(const MouseEvent& event) OVERRIDE; + virtual void OnMouseReleased(const MouseEvent& event) OVERRIDE; + virtual bool OnKeyPressed(const KeyEvent& event) OVERRIDE; + virtual bool GetDropFormats( + int* formats, + std::set<OSExchangeData::CustomFormat>* custom_formats) OVERRIDE; + virtual bool CanDrop(const OSExchangeData& data) OVERRIDE; + virtual int OnDragUpdated(const DropTargetEvent& event) OVERRIDE; + virtual int OnPerformDrop(const DropTargetEvent& event) OVERRIDE; + virtual void OnDragDone() OVERRIDE; + virtual bool OnKeyReleased(const KeyEvent& event) OVERRIDE; + virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; + virtual void OnFocus() OVERRIDE; + virtual void OnBlur() OVERRIDE; + + // TouchSelectionClientView overrides: + virtual void SelectRect(const gfx::Point& start, + const gfx::Point& end) OVERRIDE; + + // ContextMenuController overrides: + virtual void ShowContextMenuForView(View* source, + const gfx::Point& p, + bool is_mouse_gesture) OVERRIDE; + + // Overridden from DragController: + virtual void WriteDragDataForView(View* sender, + const gfx::Point& press_pt, + OSExchangeData* data) OVERRIDE; + virtual int GetDragOperationsForView(View* sender, + const gfx::Point& p) OVERRIDE; + virtual bool CanStartDragForView(View* sender, + const gfx::Point& press_pt, + const gfx::Point& p) OVERRIDE; + + // NativeTextfieldWrapper overrides: + virtual string16 GetText() const OVERRIDE; + virtual void UpdateText() OVERRIDE; + virtual void AppendText(const string16& text) OVERRIDE; + virtual string16 GetSelectedText() const OVERRIDE; + virtual void SelectAll() OVERRIDE; + virtual void ClearSelection() OVERRIDE; + virtual void UpdateBorder() OVERRIDE; + virtual void UpdateTextColor() OVERRIDE; + virtual void UpdateBackgroundColor() OVERRIDE; + virtual void UpdateReadOnly() OVERRIDE; + virtual void UpdateFont() OVERRIDE; + virtual void UpdateIsPassword() OVERRIDE; + virtual void UpdateEnabled() OVERRIDE; + virtual gfx::Insets CalculateInsets() OVERRIDE; + virtual void UpdateHorizontalMargins() OVERRIDE; + virtual void UpdateVerticalMargins() OVERRIDE; + virtual bool SetFocus() OVERRIDE; + virtual View* GetView() OVERRIDE; + virtual gfx::NativeView GetTestingHandle() const OVERRIDE; + virtual bool IsIMEComposing() const OVERRIDE; + virtual void GetSelectedRange(ui::Range* range) const OVERRIDE; + virtual void SelectRange(const ui::Range& range) OVERRIDE; + virtual void GetSelectionModel(gfx::SelectionModel* sel) const OVERRIDE; + virtual void SelectSelectionModel(const gfx::SelectionModel& sel) OVERRIDE; + virtual size_t GetCursorPosition() const OVERRIDE; + virtual bool HandleKeyPressed(const KeyEvent& e) OVERRIDE; + virtual bool HandleKeyReleased(const KeyEvent& e) OVERRIDE; + virtual void HandleFocus() OVERRIDE; + virtual void HandleBlur() OVERRIDE; + virtual ui::TextInputClient* GetTextInputClient() OVERRIDE; + virtual void ApplyStyleRange(const gfx::StyleRange& style) OVERRIDE; + virtual void ApplyDefaultStyle() OVERRIDE; + virtual void ClearEditHistory() OVERRIDE; + + // ui::SimpleMenuModel::Delegate overrides + virtual bool IsCommandIdChecked(int command_id) const OVERRIDE; + virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE; + virtual bool GetAcceleratorForCommandId( + int command_id, + ui::Accelerator* accelerator) OVERRIDE; + virtual void ExecuteCommand(int command_id) OVERRIDE; + + // class name of internal + static const char kViewClassName[]; + + protected: + // View override. + virtual void OnBoundsChanged(const gfx::Rect& previous_bounds) OVERRIDE; + + private: + friend class NativeTextfieldViewsTest; + friend class TouchSelectionControllerImplTest; + + // Overridden from ui::TextInputClient: + virtual void SetCompositionText( + const ui::CompositionText& composition) OVERRIDE; + virtual void ConfirmCompositionText() OVERRIDE; + virtual void ClearCompositionText() OVERRIDE; + virtual void InsertText(const string16& text) OVERRIDE; + virtual void InsertChar(char16 ch, int flags) OVERRIDE; + virtual ui::TextInputType GetTextInputType() const OVERRIDE; + virtual gfx::Rect GetCaretBounds() OVERRIDE; + virtual bool HasCompositionText() OVERRIDE; + virtual bool GetTextRange(ui::Range* range) OVERRIDE; + virtual bool GetCompositionTextRange(ui::Range* range) OVERRIDE; + virtual bool GetSelectionRange(ui::Range* range) OVERRIDE; + virtual bool SetSelectionRange(const ui::Range& range) OVERRIDE; + virtual bool DeleteRange(const ui::Range& range) OVERRIDE; + virtual bool GetTextFromRange(const ui::Range& range, + string16* text) OVERRIDE; + virtual void OnInputMethodChanged() OVERRIDE; + virtual bool ChangeTextDirectionAndLayoutAlignment( + base::i18n::TextDirection direction) OVERRIDE; + + // Overridden from TextfieldViewsModel::Delegate: + virtual void OnCompositionTextConfirmedOrCleared() OVERRIDE; + + // Returns the TextfieldViewsModel's text/cursor/selection rendering model. + gfx::RenderText* GetRenderText() const; + + // A callback function to periodically update the cursor state. + void UpdateCursor(); + + // Repaint the cursor. + void RepaintCursor(); + + // Update the cursor_bounds and text_offset. + void UpdateCursorBoundsAndTextOffset(size_t cursor_pos, bool insert_mode); + + void PaintTextAndCursor(gfx::Canvas* canvas); + + // Handle the keyevent. + bool HandleKeyEvent(const KeyEvent& key_event); + + // Helper function to call MoveCursorTo on the TextfieldViewsModel. + bool MoveCursorTo(const gfx::Point& point, bool select); + + // Utility function to inform the parent textfield (and its controller if any) + // that the text in the textfield has changed. + void PropagateTextChange(); + + // Does necessary updates when the text and/or the position of the cursor + // changed. + void UpdateAfterChange(bool text_changed, bool cursor_changed); + + // Utility function to prepare the context menu.. + void UpdateContextMenu(); + + // Convenience method to call InputMethod::OnTextInputTypeChanged(); + void OnTextInputTypeChanged(); + + // Convenience method to call InputMethod::OnCaretBoundsChanged(); + void OnCaretBoundsChanged(); + + // Convenience method to call TextfieldController::OnBeforeUserAction(); + void OnBeforeUserAction(); + + // Convenience method to call TextfieldController::OnAfterUserAction(); + void OnAfterUserAction(); + + // Calls |model_->Paste()| and calls TextfieldController::ContentsChanged() + // explicitly if paste succeeded. + bool Paste(); + + // Tracks the mouse clicks for single/double/triple clicks. + void TrackMouseClicks(const MouseEvent& event); + + // Handles mouse press events. + void HandleMousePressEvent(const MouseEvent& event); + + // Checks if a char is ok to be inserted into the textfield. The |ch| is a + // modified character, i.e., modifiers took effect when generating this char. + static bool ShouldInsertChar(char16 ch, int flags); + + // The parent textfield, the owner of this object. + Textfield* textfield_; + + // The text model. + scoped_ptr<TextfieldViewsModel> model_; + + // The reference to the border class. The object is owned by View::border_. + FocusableBorder* text_border_; + + // The textfield's text and drop cursor visibility. + bool is_cursor_visible_; + // The drop cursor is a visual cue for where dragged text will be dropped. + bool is_drop_cursor_visible_; + + // True if InputMethod::CancelComposition() should not be called. + bool skip_input_method_cancel_composition_; + + // Is the user potentially dragging and dropping from this view? + bool initiating_drag_; + + // A runnable method factory for callback to update the cursor. + base::WeakPtrFactory<NativeTextfieldViews> cursor_timer_; + + // State variables used to track double and triple clicks. + size_t aggregated_clicks_; + base::Time last_click_time_; + gfx::Point last_click_location_; + + // Context menu and its content list for the textfield. + scoped_ptr<ui::SimpleMenuModel> context_menu_contents_; + scoped_ptr<views::MenuModelAdapter> context_menu_delegate_; + scoped_ptr<views::MenuRunner> context_menu_runner_; + + scoped_ptr<TouchSelectionController> touch_selection_controller_; + + DISALLOW_COPY_AND_ASSIGN(NativeTextfieldViews); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_VIEWS_H_ diff --git a/ui/views/controls/textfield/native_textfield_views_unittest.cc b/ui/views/controls/textfield/native_textfield_views_unittest.cc new file mode 100644 index 0000000..1c15e7a --- /dev/null +++ b/ui/views/controls/textfield/native_textfield_views_unittest.cc @@ -0,0 +1,1573 @@ +// 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 <string> +#include <vector> + +#include "base/auto_reset.h" +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/message_loop.h" +#include "base/pickle.h" +#include "base/utf_string_conversions.h" +#include "googleurl/src/gurl.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/clipboard/scoped_clipboard_writer.h" +#include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/ime/text_input_client.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/render_text.h" +#include "ui/views/controls/textfield/native_textfield_views.h" +#include "ui/views/controls/textfield/textfield.h" +#include "ui/views/controls/textfield/textfield_controller.h" +#include "ui/views/controls/textfield/textfield_views_model.h" +#include "ui/views/events/event.h" +#include "ui/views/focus/focus_manager.h" +#include "ui/views/ime/mock_input_method.h" +#include "ui/views/test/test_views_delegate.h" +#include "ui/views/test/views_test_base.h" +#include "ui/views/widget/native_widget_private.h" +#include "ui/views/widget/widget.h" +#include "views/views_delegate.h" + +// Drag and drop for aura in linux hasn't been implemented yet. +// Bug http://crbug.com/97845 +#if defined(USE_AURA) && defined(OS_LINUX) +#define MAYBE_DragAndDrop_InitiateDrag DISABLED_DragAndDrop_InitiateDrag +#define MAYBE_DragAndDrop_ToTheLeft DISABLED_DragAndDrop_ToTheLeft +#define MAYBE_DragAndDrop_ToTheRight DISABLED_DragAndDrop_ToTheRight +#define MAYBE_DragAndDrop_Canceled DISABLED_DragAndDrop_Canceled +#else +#define MAYBE_DragAndDrop_InitiateDrag DragAndDrop_InitiateDrag +#define MAYBE_DragAndDrop_ToTheLeft DragAndDrop_ToTheLeft +#define MAYBE_DragAndDrop_ToTheRight DragAndDrop_ToTheRight +#define MAYBE_DragAndDrop_Canceled DragAndDrop_Canceled +#endif // OS_LINUX && USE_AURA + +namespace { + +// A wrapper of Textfield to intercept the result of OnKeyPressed() and +// OnKeyReleased() methods. +class TestTextfield : public views::Textfield { + public: + TestTextfield() + : key_handled_(false), + key_received_(false) { + } + + explicit TestTextfield(StyleFlags style) + : Textfield(style), + key_handled_(false), + key_received_(false) { + } + + virtual bool OnKeyPressed(const views::KeyEvent& e) OVERRIDE { + key_received_ = true; + key_handled_ = views::Textfield::OnKeyPressed(e); + return key_handled_; + } + + virtual bool OnKeyReleased(const views::KeyEvent& e) OVERRIDE { + key_received_ = true; + key_handled_ = views::Textfield::OnKeyReleased(e); + return key_handled_; + } + + bool key_handled() const { return key_handled_; } + bool key_received() const { return key_received_; } + + void clear() { + key_received_ = key_handled_ = false; + } + + private: + bool key_handled_; + bool key_received_; + + DISALLOW_COPY_AND_ASSIGN(TestTextfield); +}; + +// A helper class for use with ui::TextInputClient::GetTextFromRange(). +class GetTextHelper { + public: + GetTextHelper() { + } + + void set_text(const string16& text) { text_ = text; } + const string16& text() const { return text_; } + + private: + string16 text_; + + DISALLOW_COPY_AND_ASSIGN(GetTextHelper); +}; + +const char16 kHebrewLetterSamekh = 0x05E1; + +} // namespace + +namespace views { + +// Convert to Wide so that the printed string will be readable when +// check fails. +#define EXPECT_STR_EQ(ascii, utf16) \ + EXPECT_EQ(ASCIIToWide(ascii), UTF16ToWide(utf16)) +#define EXPECT_STR_NE(ascii, utf16) \ + EXPECT_NE(ASCIIToWide(ascii), UTF16ToWide(utf16)) + +// TODO(oshima): Move tests that are independent of TextfieldViews to +// textfield_unittests.cc once we move the test utility functions +// from chrome/browser/automation/ to ui/base/test/. +class NativeTextfieldViewsTest : public ViewsTestBase, + public TextfieldController { + public: + NativeTextfieldViewsTest() + : widget_(NULL), + textfield_(NULL), + textfield_view_(NULL), + model_(NULL), + input_method_(NULL), + on_before_user_action_(0), + on_after_user_action_(0) { + } + + // ::testing::Test: + virtual void SetUp() { + ViewsTestBase::SetUp(); + Widget::SetPureViews(true); + } + + virtual void TearDown() { + if (widget_) + widget_->Close(); + Widget::SetPureViews(false); + ViewsTestBase::TearDown(); + } + + // TextfieldController: + virtual void ContentsChanged(Textfield* sender, + const string16& new_contents) { + ASSERT_NE(last_contents_, new_contents); + last_contents_ = new_contents; + } + + virtual bool HandleKeyEvent(Textfield* sender, + const KeyEvent& key_event) { + + // TODO(oshima): figure out how to test the keystroke. + return false; + } + + virtual void OnBeforeUserAction(Textfield* sender) { + ++on_before_user_action_; + } + + virtual void OnAfterUserAction(Textfield* sender) { + ++on_after_user_action_; + } + + void InitTextfield(Textfield::StyleFlags style) { + InitTextfields(style, 1); + } + + void InitTextfields(Textfield::StyleFlags style, int count) { + ASSERT_FALSE(textfield_); + textfield_ = new TestTextfield(style); + textfield_->SetController(this); + widget_ = new Widget; + Widget::InitParams params(Widget::InitParams::TYPE_POPUP); + params.bounds = gfx::Rect(100, 100, 100, 100); + widget_->Init(params); + View* container = new View(); + widget_->SetContentsView(container); + container->AddChildView(textfield_); + + textfield_view_ = static_cast<NativeTextfieldViews*>( + textfield_->GetNativeWrapperForTesting()); + textfield_view_->SetBoundsRect(params.bounds); + textfield_->set_id(1); + + for (int i = 1; i < count; i++) { + Textfield* textfield = new Textfield(style); + container->AddChildView(textfield); + textfield->set_id(i + 1); + } + + DCHECK(textfield_view_); + model_ = textfield_view_->model_.get(); + model_->ClearEditHistory(); + + input_method_ = new MockInputMethod(); + widget_->ReplaceInputMethod(input_method_); + + // Assumes the Widget is always focused. + input_method_->OnFocus(); + + textfield_->RequestFocus(); + } + + ui::MenuModel* GetContextMenuModel() { + textfield_view_->UpdateContextMenu(); + return textfield_view_->context_menu_contents_.get(); + } + + protected: + void SendKeyEvent(ui::KeyboardCode key_code, + bool shift, + bool control, + bool caps_lock) { + int flags = (shift ? ui::EF_SHIFT_DOWN : 0) | + (control ? ui::EF_CONTROL_DOWN : 0) | + (caps_lock ? ui::EF_CAPS_LOCK_DOWN : 0); + KeyEvent event(ui::ET_KEY_PRESSED, key_code, flags); + input_method_->DispatchKeyEvent(event); + } + + void SendKeyEvent(ui::KeyboardCode key_code, bool shift, bool control) { + SendKeyEvent(key_code, shift, control, false); + } + + void SendKeyEvent(ui::KeyboardCode key_code) { + SendKeyEvent(key_code, false, false); + } + + void SendKeyEvent(char16 ch) { + if (ch < 0x80) { + ui::KeyboardCode code = + ch == ' ' ? ui::VKEY_SPACE : + static_cast<ui::KeyboardCode>(ui::VKEY_A + ch - 'a'); + SendKeyEvent(code); + } else { + KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_UNKNOWN, 0); + event.set_character(ch); + input_method_->DispatchKeyEvent(event); + } + } + + string16 GetClipboardText() const { + string16 text; + views::ViewsDelegate::views_delegate->GetClipboard()-> + ReadText(ui::Clipboard::BUFFER_STANDARD, &text); + return text; + } + + void SetClipboardText(const std::string& text) { + ui::ScopedClipboardWriter clipboard_writer( + views::ViewsDelegate::views_delegate->GetClipboard()); + clipboard_writer.WriteText(ASCIIToUTF16(text)); + } + + View* GetFocusedView() { + return widget_->GetFocusManager()->GetFocusedView(); + } + + int GetCursorPositionX(int cursor_pos) { + gfx::RenderText* render_text = textfield_view_->GetRenderText(); + return render_text->GetCursorBounds( + gfx::SelectionModel(cursor_pos), false).x(); + } + + // Get the current cursor bounds. + gfx::Rect GetCursorBounds() { + gfx::RenderText* render_text = textfield_view_->GetRenderText(); + gfx::Rect bounds = render_text->GetUpdatedCursorBounds(); + return bounds; + } + + // Get the cursor bounds of |sel|. + gfx::Rect GetCursorBounds(const gfx::SelectionModel& sel) { + gfx::RenderText* render_text = textfield_view_->GetRenderText(); + gfx::Rect bounds = render_text->GetCursorBounds(sel, true); + return bounds; + } + + gfx::Rect GetDisplayRect() { + return textfield_view_->GetRenderText()->display_rect(); + } + + // Mouse click on the point whose x-axis is |bound|'s x plus |x_offset| and + // y-axis is in the middle of |bound|'s vertical range. + void MouseClick(const gfx::Rect bound, int x_offset) { + int x = bound.x() + x_offset; + int y = bound.y() + bound.height() / 2; + MouseEvent click(ui::ET_MOUSE_PRESSED, x, y, ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMousePressed(click); + MouseEvent release(ui::ET_MOUSE_RELEASED, x, y, ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMouseReleased(release); + } + + // This is to avoid double/triple click. + void NonClientMouseClick() { + MouseEvent click(ui::ET_MOUSE_PRESSED, 0, 0, + ui::EF_LEFT_BUTTON_DOWN | ui::EF_IS_NON_CLIENT); + textfield_view_->OnMousePressed(click); + MouseEvent release(ui::ET_MOUSE_RELEASED, 0, 0, + ui::EF_LEFT_BUTTON_DOWN | ui::EF_IS_NON_CLIENT); + textfield_view_->OnMouseReleased(release); + } + + // Wrap for visibility in test classes. + ui::TextInputType GetTextInputType() { + return textfield_view_->GetTextInputType(); + } + + void VerifyTextfieldContextMenuContents(bool textfield_has_selection, + ui::MenuModel* menu_model) { + EXPECT_TRUE(menu_model->IsEnabledAt(4 /* Separator */)); + EXPECT_TRUE(menu_model->IsEnabledAt(5 /* SELECT ALL */)); + EXPECT_EQ(textfield_has_selection, menu_model->IsEnabledAt(0 /* CUT */)); + EXPECT_EQ(textfield_has_selection, menu_model->IsEnabledAt(1 /* COPY */)); + EXPECT_EQ(textfield_has_selection, menu_model->IsEnabledAt(3 /* DELETE */)); + string16 str(GetClipboardText()); + EXPECT_NE(str.empty(), menu_model->IsEnabledAt(2 /* PASTE */)); + } + + // We need widget to populate wrapper class. + Widget* widget_; + + TestTextfield* textfield_; + NativeTextfieldViews* textfield_view_; + TextfieldViewsModel* model_; + + // The string from Controller::ContentsChanged callback. + string16 last_contents_; + + // For testing input method related behaviors. + MockInputMethod* input_method_; + + // Indicates how many times OnBeforeUserAction() is called. + int on_before_user_action_; + + // Indicates how many times OnAfterUserAction() is called. + int on_after_user_action_; + + private: + DISALLOW_COPY_AND_ASSIGN(NativeTextfieldViewsTest); +}; + +TEST_F(NativeTextfieldViewsTest, ModelChangesTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + + // TextfieldController::ContentsChanged() shouldn't be called when changing + // text programmatically. + last_contents_.clear(); + textfield_->SetText(ASCIIToUTF16("this is")); + + EXPECT_STR_EQ("this is", model_->GetText()); + EXPECT_STR_EQ("this is", textfield_->text()); + EXPECT_TRUE(last_contents_.empty()); + + textfield_->AppendText(ASCIIToUTF16(" a test")); + EXPECT_STR_EQ("this is a test", model_->GetText()); + EXPECT_STR_EQ("this is a test", textfield_->text()); + EXPECT_TRUE(last_contents_.empty()); + + EXPECT_EQ(string16(), textfield_->GetSelectedText()); + textfield_->SelectAll(); + EXPECT_STR_EQ("this is a test", textfield_->GetSelectedText()); + EXPECT_TRUE(last_contents_.empty()); +} + +TEST_F(NativeTextfieldViewsTest, KeyTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + SendKeyEvent(ui::VKEY_C, true, false); + EXPECT_STR_EQ("C", textfield_->text()); + EXPECT_STR_EQ("C", last_contents_); + last_contents_.clear(); + + SendKeyEvent(ui::VKEY_R, false, false); + EXPECT_STR_EQ("Cr", textfield_->text()); + EXPECT_STR_EQ("Cr", last_contents_); + + textfield_->SetText(ASCIIToUTF16("")); + SendKeyEvent(ui::VKEY_C, true, false, true); + SendKeyEvent(ui::VKEY_C, false, false, true); + SendKeyEvent(ui::VKEY_1, false, false, true); + SendKeyEvent(ui::VKEY_1, true, false, true); + SendKeyEvent(ui::VKEY_1, true, false, false); + EXPECT_STR_EQ("cC1!!", textfield_->text()); + EXPECT_STR_EQ("cC1!!", last_contents_); +} + +TEST_F(NativeTextfieldViewsTest, ControlAndSelectTest) { + // Insert a test string in a textfield. + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("one two three")); + SendKeyEvent(ui::VKEY_RIGHT, + true /* shift */, false /* control */); + SendKeyEvent(ui::VKEY_RIGHT, true, false); + SendKeyEvent(ui::VKEY_RIGHT, true, false); + + EXPECT_STR_EQ("one", textfield_->GetSelectedText()); + + // Test word select. + SendKeyEvent(ui::VKEY_RIGHT, true, true); + EXPECT_STR_EQ("one two", textfield_->GetSelectedText()); + SendKeyEvent(ui::VKEY_RIGHT, true, true); + EXPECT_STR_EQ("one two three", textfield_->GetSelectedText()); + SendKeyEvent(ui::VKEY_LEFT, true, true); + EXPECT_STR_EQ("one two ", textfield_->GetSelectedText()); + SendKeyEvent(ui::VKEY_LEFT, true, true); + EXPECT_STR_EQ("one ", textfield_->GetSelectedText()); + + // Replace the selected text. + SendKeyEvent(ui::VKEY_Z, true, false); + SendKeyEvent(ui::VKEY_E, true, false); + SendKeyEvent(ui::VKEY_R, true, false); + SendKeyEvent(ui::VKEY_O, true, false); + SendKeyEvent(ui::VKEY_SPACE, false, false); + EXPECT_STR_EQ("ZERO two three", textfield_->text()); + + SendKeyEvent(ui::VKEY_END, true, false); + EXPECT_STR_EQ("two three", textfield_->GetSelectedText()); + SendKeyEvent(ui::VKEY_HOME, true, false); + EXPECT_STR_EQ("ZERO ", textfield_->GetSelectedText()); +} + +TEST_F(NativeTextfieldViewsTest, InsertionDeletionTest) { + // Insert a test string in a textfield. + InitTextfield(Textfield::STYLE_DEFAULT); + char test_str[] = "this is a test"; + for (size_t i = 0; i < sizeof(test_str); i++) { + // This is ugly and should be replaced by a utility standard function. + // See comment in NativeTextfieldViews::GetPrintableChar. + char c = test_str[i]; + ui::KeyboardCode code = + c == ' ' ? ui::VKEY_SPACE : + static_cast<ui::KeyboardCode>(ui::VKEY_A + c - 'a'); + SendKeyEvent(code); + } + EXPECT_STR_EQ(test_str, textfield_->text()); + + // Move the cursor around. + for (int i = 0; i < 6; i++) { + SendKeyEvent(ui::VKEY_LEFT); + } + SendKeyEvent(ui::VKEY_RIGHT); + + // Delete using backspace and check resulting string. + SendKeyEvent(ui::VKEY_BACK); + EXPECT_STR_EQ("this is test", textfield_->text()); + + // Delete using delete key and check resulting string. + for (int i = 0; i < 5; i++) { + SendKeyEvent(ui::VKEY_DELETE); + } + EXPECT_STR_EQ("this is ", textfield_->text()); + + // Select all and replace with "k". + textfield_->SelectAll(); + SendKeyEvent(ui::VKEY_K); + EXPECT_STR_EQ("k", textfield_->text()); + + // Delete the previous word from cursor. + textfield_->SetText(ASCIIToUTF16("one two three four")); + SendKeyEvent(ui::VKEY_END); + SendKeyEvent(ui::VKEY_BACK, false, true, false); + EXPECT_STR_EQ("one two three ", textfield_->text()); + + // Delete upto the beginning of the buffer from cursor in chromeos, do nothing + // in windows. + SendKeyEvent(ui::VKEY_LEFT, false, true, false); + SendKeyEvent(ui::VKEY_BACK, true, true, false); +#if defined(OS_WIN) + EXPECT_STR_EQ("one two three ", textfield_->text()); +#else + EXPECT_STR_EQ("three ", textfield_->text()); +#endif + + // Delete the next word from cursor. + textfield_->SetText(ASCIIToUTF16("one two three four")); + SendKeyEvent(ui::VKEY_HOME); + SendKeyEvent(ui::VKEY_DELETE, false, true, false); + EXPECT_STR_EQ(" two three four", textfield_->text()); + + // Delete upto the end of the buffer from cursor in chromeos, do nothing + // in windows. + SendKeyEvent(ui::VKEY_RIGHT, false, true, false); + SendKeyEvent(ui::VKEY_DELETE, true, true, false); +#if defined(OS_WIN) + EXPECT_STR_EQ(" two three four", textfield_->text()); +#else + EXPECT_STR_EQ(" two", textfield_->text()); +#endif +} + +TEST_F(NativeTextfieldViewsTest, PasswordTest) { + InitTextfield(Textfield::STYLE_PASSWORD); + + EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, GetTextInputType()); + + last_contents_.clear(); + textfield_->SetText(ASCIIToUTF16("my password")); + // Just to make sure the text() and callback returns + // the actual text instead of "*". + EXPECT_STR_EQ("my password", textfield_->text()); + EXPECT_TRUE(last_contents_.empty()); +} + +TEST_F(NativeTextfieldViewsTest, InputTypeSetsPassword) { + InitTextfield(Textfield::STYLE_DEFAULT); + + // Defaults to TEXT + EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, GetTextInputType()); + + // Setting to passwords also sets password state of textfield. + textfield_->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, GetTextInputType()); + EXPECT_TRUE(textfield_->IsPassword()); +} + +TEST_F(NativeTextfieldViewsTest, PasswordSetsInputType) { + InitTextfield(Textfield::STYLE_DEFAULT); + + // Defaults to TEXT + EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, GetTextInputType()); + + textfield_->SetPassword(true); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, GetTextInputType()); + + textfield_->SetPassword(false); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, GetTextInputType()); +} + +TEST_F(NativeTextfieldViewsTest, TextInputType) { + InitTextfield(Textfield::STYLE_DEFAULT); + + // Defaults to TEXT + EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, GetTextInputType()); + + // And can be set. + textfield_->SetTextInputType(ui::TEXT_INPUT_TYPE_URL); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_URL, GetTextInputType()); + + // Readonly textfields have type NONE + textfield_->SetReadOnly(true); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, GetTextInputType()); + + textfield_->SetReadOnly(false); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_URL, GetTextInputType()); + + // As do disabled textfields + textfield_->SetEnabled(false); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, GetTextInputType()); +} + +TEST_F(NativeTextfieldViewsTest, OnKeyPressReturnValueTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + + // Character keys will be handled by input method. + SendKeyEvent(ui::VKEY_A); + EXPECT_TRUE(textfield_->key_received()); + EXPECT_FALSE(textfield_->key_handled()); + textfield_->clear(); + + // Home will be handled. + SendKeyEvent(ui::VKEY_HOME); + EXPECT_TRUE(textfield_->key_received()); + EXPECT_TRUE(textfield_->key_handled()); + textfield_->clear(); + + // F24, up/down key won't be handled. + SendKeyEvent(ui::VKEY_F24); + EXPECT_TRUE(textfield_->key_received()); + EXPECT_FALSE(textfield_->key_handled()); + textfield_->clear(); + + SendKeyEvent(ui::VKEY_UP); + EXPECT_TRUE(textfield_->key_received()); + EXPECT_FALSE(textfield_->key_handled()); + textfield_->clear(); + + SendKeyEvent(ui::VKEY_DOWN); + EXPECT_TRUE(textfield_->key_received()); + EXPECT_FALSE(textfield_->key_handled()); +} + +TEST_F(NativeTextfieldViewsTest, CursorMovement) { + InitTextfield(Textfield::STYLE_DEFAULT); + + // Test with trailing whitespace. + textfield_->SetText(ASCIIToUTF16("one two hre ")); + + // Send the cursor at the end. + SendKeyEvent(ui::VKEY_END); + + // Ctrl+Left should move the cursor just before the last word. + SendKeyEvent(ui::VKEY_LEFT, false, true); + SendKeyEvent(ui::VKEY_T); + EXPECT_STR_EQ("one two thre ", textfield_->text()); + EXPECT_STR_EQ("one two thre ", last_contents_); + + // Ctrl+Right should move the cursor to the end of the last word. + SendKeyEvent(ui::VKEY_RIGHT, false, true); + SendKeyEvent(ui::VKEY_E); + EXPECT_STR_EQ("one two three ", textfield_->text()); + EXPECT_STR_EQ("one two three ", last_contents_); + + // Ctrl+Right again should move the cursor to the end. + SendKeyEvent(ui::VKEY_RIGHT, false, true); + SendKeyEvent(ui::VKEY_BACK); + EXPECT_STR_EQ("one two three", textfield_->text()); + EXPECT_STR_EQ("one two three", last_contents_); + + // Test with leading whitespace. + textfield_->SetText(ASCIIToUTF16(" ne two")); + + // Send the cursor at the beginning. + SendKeyEvent(ui::VKEY_HOME); + + // Ctrl+Right, then Ctrl+Left should move the cursor to the beginning of the + // first word. + SendKeyEvent(ui::VKEY_RIGHT, false, true); + SendKeyEvent(ui::VKEY_LEFT, false, true); + SendKeyEvent(ui::VKEY_O); + EXPECT_STR_EQ(" one two", textfield_->text()); + EXPECT_STR_EQ(" one two", last_contents_); + + // Ctrl+Left to move the cursor to the beginning of the first word. + SendKeyEvent(ui::VKEY_LEFT, false, true); + // Ctrl+Left again should move the cursor back to the very beginning. + SendKeyEvent(ui::VKEY_LEFT, false, true); + SendKeyEvent(ui::VKEY_DELETE); + EXPECT_STR_EQ("one two", textfield_->text()); + EXPECT_STR_EQ("one two", last_contents_); +} + +TEST_F(NativeTextfieldViewsTest, FocusTraversalTest) { + InitTextfields(Textfield::STYLE_DEFAULT, 3); + textfield_->RequestFocus(); + + EXPECT_EQ(1, GetFocusedView()->id()); + widget_->GetFocusManager()->AdvanceFocus(false); + EXPECT_EQ(2, GetFocusedView()->id()); + widget_->GetFocusManager()->AdvanceFocus(false); + EXPECT_EQ(3, GetFocusedView()->id()); + // Cycle back to the first textfield. + widget_->GetFocusManager()->AdvanceFocus(false); + EXPECT_EQ(1, GetFocusedView()->id()); + + widget_->GetFocusManager()->AdvanceFocus(true); + EXPECT_EQ(3, GetFocusedView()->id()); + widget_->GetFocusManager()->AdvanceFocus(true); + EXPECT_EQ(2, GetFocusedView()->id()); + widget_->GetFocusManager()->AdvanceFocus(true); + EXPECT_EQ(1, GetFocusedView()->id()); + // Cycle back to the last textfield. + widget_->GetFocusManager()->AdvanceFocus(true); + EXPECT_EQ(3, GetFocusedView()->id()); + + // Request focus should still work. + textfield_->RequestFocus(); + EXPECT_EQ(1, GetFocusedView()->id()); + + // Test if clicking on textfield view sets the focus to textfield_. + widget_->GetFocusManager()->AdvanceFocus(true); + EXPECT_EQ(3, GetFocusedView()->id()); + MouseEvent click(ui::ET_MOUSE_PRESSED, 0, 0, ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMousePressed(click); + EXPECT_EQ(1, GetFocusedView()->id()); +} + +TEST_F(NativeTextfieldViewsTest, ContextMenuDisplayTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("hello world")); + EXPECT_TRUE(GetContextMenuModel()); + VerifyTextfieldContextMenuContents(false, GetContextMenuModel()); + + textfield_->SelectAll(); + VerifyTextfieldContextMenuContents(true, GetContextMenuModel()); +} + +TEST_F(NativeTextfieldViewsTest, DoubleAndTripleClickTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("hello world")); + MouseEvent click(ui::ET_MOUSE_PRESSED, 0, 0, ui::EF_LEFT_BUTTON_DOWN); + MouseEvent release(ui::ET_MOUSE_RELEASED, 0, 0, ui::EF_LEFT_BUTTON_DOWN); + MouseEvent double_click(ui::ET_MOUSE_PRESSED, 0, 0, + ui::EF_LEFT_BUTTON_DOWN | ui::EF_IS_DOUBLE_CLICK); + + // Test for double click. + textfield_view_->OnMousePressed(click); + textfield_view_->OnMouseReleased(release); + EXPECT_TRUE(textfield_->GetSelectedText().empty()); + textfield_view_->OnMousePressed(double_click); + textfield_view_->OnMouseReleased(release); + EXPECT_STR_EQ("hello", textfield_->GetSelectedText()); + + // Test for triple click. + textfield_view_->OnMousePressed(click); + textfield_view_->OnMouseReleased(release); + EXPECT_STR_EQ("hello world", textfield_->GetSelectedText()); + + // Another click should reset back to single click. + textfield_view_->OnMousePressed(click); + textfield_view_->OnMouseReleased(release); + EXPECT_TRUE(textfield_->GetSelectedText().empty()); +} + +TEST_F(NativeTextfieldViewsTest, DragToSelect) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("hello world")); + const int kStart = GetCursorPositionX(5); + const int kEnd = 500; + MouseEvent click_a(ui::ET_MOUSE_PRESSED, kStart, 0, ui::EF_LEFT_BUTTON_DOWN); + MouseEvent click_b(ui::ET_MOUSE_PRESSED, kEnd, 0, ui::EF_LEFT_BUTTON_DOWN); + MouseEvent drag_left(ui::ET_MOUSE_DRAGGED, 0, 0, ui::EF_LEFT_BUTTON_DOWN); + MouseEvent drag_right(ui::ET_MOUSE_DRAGGED, kEnd, 0, ui::EF_LEFT_BUTTON_DOWN); + MouseEvent release(ui::ET_MOUSE_RELEASED, kEnd, 0, ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMousePressed(click_a); + EXPECT_TRUE(textfield_->GetSelectedText().empty()); + // Check that dragging left selects the beginning of the string. + textfield_view_->OnMouseDragged(drag_left); + string16 text_left = textfield_->GetSelectedText(); + EXPECT_STR_EQ("hello", text_left); + // Check that dragging right selects the rest of the string. + textfield_view_->OnMouseDragged(drag_right); + string16 text_right = textfield_->GetSelectedText(); + EXPECT_STR_EQ(" world", text_right); + // Check that releasing in the same location does not alter the selection. + textfield_view_->OnMouseReleased(release); + EXPECT_EQ(text_right, textfield_->GetSelectedText()); + // Check that dragging from beyond the text length works too. + textfield_view_->OnMousePressed(click_b); + textfield_view_->OnMouseDragged(drag_left); + textfield_view_->OnMouseReleased(release); + EXPECT_EQ(textfield_->text(), textfield_->GetSelectedText()); +} + +#if defined(OS_WIN) || defined(TOOLKIT_USES_GTK) +TEST_F(NativeTextfieldViewsTest, DragAndDrop_AcceptDrop) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("hello world")); + + ui::OSExchangeData data; + string16 string(ASCIIToUTF16("string ")); + data.SetString(string); + int formats = 0; + std::set<OSExchangeData::CustomFormat> custom_formats; + + // Ensure that disabled textfields do not accept drops. + textfield_->SetEnabled(false); + EXPECT_FALSE(textfield_view_->GetDropFormats(&formats, &custom_formats)); + EXPECT_EQ(0, formats); + EXPECT_TRUE(custom_formats.empty()); + EXPECT_FALSE(textfield_view_->CanDrop(data)); + textfield_->SetEnabled(true); + + // Ensure that read-only textfields do not accept drops. + textfield_->SetReadOnly(true); + EXPECT_FALSE(textfield_view_->GetDropFormats(&formats, &custom_formats)); + EXPECT_EQ(0, formats); + EXPECT_TRUE(custom_formats.empty()); + EXPECT_FALSE(textfield_view_->CanDrop(data)); + textfield_->SetReadOnly(false); + + // Ensure that enabled and editable textfields do accept drops. + EXPECT_TRUE(textfield_view_->GetDropFormats(&formats, &custom_formats)); + EXPECT_EQ(ui::OSExchangeData::STRING, formats); + EXPECT_TRUE(custom_formats.empty()); + EXPECT_TRUE(textfield_view_->CanDrop(data)); + DropTargetEvent drop(data, GetCursorPositionX(6), 0, + ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE); + EXPECT_EQ(ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE, + textfield_view_->OnDragUpdated(drop)); + EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, textfield_view_->OnPerformDrop(drop)); + EXPECT_STR_EQ("hello string world", textfield_->text()); + + // Ensure that textfields do not accept non-OSExchangeData::STRING types. + ui::OSExchangeData bad_data; + bad_data.SetFilename(FilePath(FILE_PATH_LITERAL("x"))); +#if defined(OS_WIN) + bad_data.SetPickledData(CF_BITMAP, Pickle()); + bad_data.SetFileContents(FilePath(L"x"), "x"); + bad_data.SetHtml(string16(ASCIIToUTF16("x")), GURL("x.org")); + ui::OSExchangeData::DownloadFileInfo download(FilePath(), NULL); + bad_data.SetDownloadFileInfo(download); +#else + // Skip OSExchangeDataProviderWin::SetURL, which also sets CF_TEXT / STRING. + bad_data.SetURL(GURL("x.org"), string16(ASCIIToUTF16("x"))); + bad_data.SetPickledData(GDK_SELECTION_PRIMARY, Pickle()); +#endif + EXPECT_FALSE(textfield_view_->CanDrop(bad_data)); +} +#endif + +TEST_F(NativeTextfieldViewsTest, MAYBE_DragAndDrop_InitiateDrag) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("hello string world")); + + // Ensure the textfield will provide selected text for drag data. + string16 string; + ui::OSExchangeData data; + const ui::Range kStringRange(6, 12); + textfield_->SelectRange(kStringRange); + const gfx::Point kStringPoint(GetCursorPositionX(9), 0); + textfield_view_->WriteDragDataForView(NULL, kStringPoint, &data); + EXPECT_TRUE(data.GetString(&string)); + EXPECT_EQ(textfield_->GetSelectedText(), string); + + // Ensure that disabled textfields do not support drag operations. + textfield_->SetEnabled(false); + EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, + textfield_view_->GetDragOperationsForView(NULL, kStringPoint)); + textfield_->SetEnabled(true); + // Ensure that textfields without selections do not support drag operations. + textfield_->ClearSelection(); + EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, + textfield_view_->GetDragOperationsForView(NULL, kStringPoint)); + textfield_->SelectRange(kStringRange); + // Ensure that textfields only initiate drag operations inside the selection. + EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, + textfield_view_->GetDragOperationsForView(NULL, gfx::Point())); + EXPECT_FALSE(textfield_view_->CanStartDragForView(NULL, gfx::Point(), + gfx::Point())); + EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, + textfield_view_->GetDragOperationsForView(NULL, kStringPoint)); + EXPECT_TRUE(textfield_view_->CanStartDragForView(NULL, kStringPoint, + gfx::Point())); + // Ensure that textfields support local moves. + EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE | ui::DragDropTypes::DRAG_COPY, + textfield_view_->GetDragOperationsForView(textfield_view_, kStringPoint)); +} + +TEST_F(NativeTextfieldViewsTest, MAYBE_DragAndDrop_ToTheRight) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("hello world")); + + string16 string; + ui::OSExchangeData data; + int formats = 0; + int operations = 0; + std::set<OSExchangeData::CustomFormat> custom_formats; + + // Start dragging "ello". + textfield_->SelectRange(ui::Range(1, 5)); + MouseEvent click_a(ui::ET_MOUSE_PRESSED, GetCursorPositionX(3), 0, + ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMousePressed(click_a); + EXPECT_TRUE(textfield_view_->CanStartDragForView(textfield_view_, + click_a.location(), gfx::Point())); + operations = textfield_view_->GetDragOperationsForView(textfield_view_, + click_a.location()); + EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE | ui::DragDropTypes::DRAG_COPY, + operations); + textfield_view_->WriteDragDataForView(NULL, click_a.location(), &data); + EXPECT_TRUE(data.GetString(&string)); + EXPECT_EQ(textfield_->GetSelectedText(), string); + EXPECT_TRUE(textfield_view_->GetDropFormats(&formats, &custom_formats)); + EXPECT_EQ(ui::OSExchangeData::STRING, formats); + EXPECT_TRUE(custom_formats.empty()); + + // Drop "ello" after "w". + const gfx::Point kDropPoint(GetCursorPositionX(7), 0); + EXPECT_TRUE(textfield_view_->CanDrop(data)); + DropTargetEvent drop_a(data, kDropPoint.x(), 0, operations); + EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE, + textfield_view_->OnDragUpdated(drop_a)); + EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE, + textfield_view_->OnPerformDrop(drop_a)); + EXPECT_STR_EQ("h welloorld", textfield_->text()); + textfield_view_->OnDragDone(); + + // Undo/Redo the drag&drop change. + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("hello world", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("hello world", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("h welloorld", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("h welloorld", textfield_->text()); +} + +TEST_F(NativeTextfieldViewsTest, MAYBE_DragAndDrop_ToTheLeft) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("hello world")); + + string16 string; + ui::OSExchangeData data; + int formats = 0; + int operations = 0; + std::set<OSExchangeData::CustomFormat> custom_formats; + + // Start dragging " worl". + textfield_->SelectRange(ui::Range(5, 10)); + MouseEvent click_a(ui::ET_MOUSE_PRESSED, GetCursorPositionX(7), 0, + ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMousePressed(click_a); + EXPECT_TRUE(textfield_view_->CanStartDragForView(textfield_view_, + click_a.location(), gfx::Point())); + operations = textfield_view_->GetDragOperationsForView(textfield_view_, + click_a.location()); + EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE | ui::DragDropTypes::DRAG_COPY, + operations); + textfield_view_->WriteDragDataForView(NULL, click_a.location(), &data); + EXPECT_TRUE(data.GetString(&string)); + EXPECT_EQ(textfield_->GetSelectedText(), string); + EXPECT_TRUE(textfield_view_->GetDropFormats(&formats, &custom_formats)); + EXPECT_EQ(ui::OSExchangeData::STRING, formats); + EXPECT_TRUE(custom_formats.empty()); + + // Drop " worl" after "h". + EXPECT_TRUE(textfield_view_->CanDrop(data)); + DropTargetEvent drop_a(data, GetCursorPositionX(1), 0, operations); + EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE, + textfield_view_->OnDragUpdated(drop_a)); + EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE, + textfield_view_->OnPerformDrop(drop_a)); + EXPECT_STR_EQ("h worlellod", textfield_->text()); + textfield_view_->OnDragDone(); + + // Undo/Redo the drag&drop change. + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("hello world", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("hello world", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("h worlellod", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("h worlellod", textfield_->text()); +} + +TEST_F(NativeTextfieldViewsTest, MAYBE_DragAndDrop_Canceled) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("hello world")); + + // Start dragging "worl". + textfield_->SelectRange(ui::Range(6, 10)); + MouseEvent click(ui::ET_MOUSE_PRESSED, GetCursorPositionX(8), 0, + ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMousePressed(click); + ui::OSExchangeData data; + textfield_view_->WriteDragDataForView(NULL, click.location(), &data); + EXPECT_TRUE(textfield_view_->CanDrop(data)); + // Drag the text over somewhere valid, outside the current selection. + DropTargetEvent drop(data, GetCursorPositionX(2), 0, + ui::DragDropTypes::DRAG_MOVE); + EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE, textfield_view_->OnDragUpdated(drop)); + // "Cancel" the drag, via move and release over the selection, and OnDragDone. + MouseEvent drag(ui::ET_MOUSE_DRAGGED, GetCursorPositionX(9), 0, + ui::EF_LEFT_BUTTON_DOWN); + MouseEvent release(ui::ET_MOUSE_RELEASED, GetCursorPositionX(9), 0, + ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMouseDragged(drag); + textfield_view_->OnMouseReleased(release); + textfield_view_->OnDragDone(); + EXPECT_EQ(ASCIIToUTF16("hello world"), textfield_->text()); +} + +TEST_F(NativeTextfieldViewsTest, ReadOnlyTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16(" one two three ")); + textfield_->SetReadOnly(true); + SendKeyEvent(ui::VKEY_HOME); + EXPECT_EQ(0U, textfield_->GetCursorPosition()); + + SendKeyEvent(ui::VKEY_END); + EXPECT_EQ(15U, textfield_->GetCursorPosition()); + + SendKeyEvent(ui::VKEY_LEFT, false, false); + EXPECT_EQ(14U, textfield_->GetCursorPosition()); + + SendKeyEvent(ui::VKEY_LEFT, false, true); + EXPECT_EQ(9U, textfield_->GetCursorPosition()); + + SendKeyEvent(ui::VKEY_LEFT, true, true); + EXPECT_EQ(5U, textfield_->GetCursorPosition()); + EXPECT_STR_EQ("two ", textfield_->GetSelectedText()); + + textfield_->SelectAll(); + EXPECT_STR_EQ(" one two three ", textfield_->GetSelectedText()); + + // CUT&PASTE does not work, but COPY works + SetClipboardText("Test"); + SendKeyEvent(ui::VKEY_X, false, true); + EXPECT_STR_EQ(" one two three ", textfield_->GetSelectedText()); + string16 str(GetClipboardText()); + EXPECT_STR_NE(" one two three ", str); + + SendKeyEvent(ui::VKEY_C, false, true); + views::ViewsDelegate::views_delegate->GetClipboard()-> + ReadText(ui::Clipboard::BUFFER_STANDARD, &str); + EXPECT_STR_EQ(" one two three ", str); + + // SetText should work even in read only mode. + textfield_->SetText(ASCIIToUTF16(" four five six ")); + EXPECT_STR_EQ(" four five six ", textfield_->text()); + + // Paste shouldn't work. + SendKeyEvent(ui::VKEY_V, false, true); + EXPECT_STR_EQ(" four five six ", textfield_->text()); + EXPECT_TRUE(textfield_->GetSelectedText().empty()); + + textfield_->SelectAll(); + EXPECT_STR_EQ(" four five six ", textfield_->GetSelectedText()); + + // Text field is unmodifiable and selection shouldn't change. + SendKeyEvent(ui::VKEY_DELETE); + EXPECT_STR_EQ(" four five six ", textfield_->GetSelectedText()); + SendKeyEvent(ui::VKEY_BACK); + EXPECT_STR_EQ(" four five six ", textfield_->GetSelectedText()); + SendKeyEvent(ui::VKEY_T); + EXPECT_STR_EQ(" four five six ", textfield_->GetSelectedText()); +} + +TEST_F(NativeTextfieldViewsTest, TextInputClientTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + ui::TextInputClient* client = textfield_->GetTextInputClient(); + EXPECT_TRUE(client); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, client->GetTextInputType()); + + textfield_->SetText(ASCIIToUTF16("0123456789")); + ui::Range range; + EXPECT_TRUE(client->GetTextRange(&range)); + EXPECT_EQ(0U, range.start()); + EXPECT_EQ(10U, range.end()); + + EXPECT_TRUE(client->SetSelectionRange(ui::Range(1, 4))); + EXPECT_TRUE(client->GetSelectionRange(&range)); + EXPECT_EQ(ui::Range(1,4), range); + + // This code can't be compiled because of a bug in base::Callback. +#if 0 + GetTextHelper helper; + base::Callback<void(string16)> callback = + base::Bind(&GetTextHelper::set_text, base::Unretained(&helper)); + + EXPECT_TRUE(client->GetTextFromRange(range, callback)); + EXPECT_STR_EQ("123", helper.text()); +#endif + + EXPECT_TRUE(client->DeleteRange(range)); + EXPECT_STR_EQ("0456789", textfield_->text()); + + ui::CompositionText composition; + composition.text = UTF8ToUTF16("321"); + // Set composition through input method. + input_method_->Clear(); + input_method_->SetCompositionTextForNextKey(composition); + textfield_->clear(); + + on_before_user_action_ = on_after_user_action_ = 0; + SendKeyEvent(ui::VKEY_A); + EXPECT_TRUE(textfield_->key_received()); + EXPECT_FALSE(textfield_->key_handled()); + EXPECT_TRUE(client->HasCompositionText()); + EXPECT_TRUE(client->GetCompositionTextRange(&range)); + EXPECT_STR_EQ("0321456789", textfield_->text()); + EXPECT_EQ(ui::Range(1,4), range); + EXPECT_EQ(2, on_before_user_action_); + EXPECT_EQ(2, on_after_user_action_); + + input_method_->SetResultTextForNextKey(UTF8ToUTF16("123")); + on_before_user_action_ = on_after_user_action_ = 0; + textfield_->clear(); + SendKeyEvent(ui::VKEY_A); + EXPECT_TRUE(textfield_->key_received()); + EXPECT_FALSE(textfield_->key_handled()); + EXPECT_FALSE(client->HasCompositionText()); + EXPECT_FALSE(input_method_->cancel_composition_called()); + EXPECT_STR_EQ("0123456789", textfield_->text()); + EXPECT_EQ(2, on_before_user_action_); + EXPECT_EQ(2, on_after_user_action_); + + input_method_->Clear(); + input_method_->SetCompositionTextForNextKey(composition); + textfield_->clear(); + SendKeyEvent(ui::VKEY_A); + EXPECT_TRUE(client->HasCompositionText()); + EXPECT_STR_EQ("0123321456789", textfield_->text()); + + on_before_user_action_ = on_after_user_action_ = 0; + textfield_->clear(); + SendKeyEvent(ui::VKEY_RIGHT); + EXPECT_FALSE(client->HasCompositionText()); + EXPECT_TRUE(input_method_->cancel_composition_called()); + EXPECT_TRUE(textfield_->key_received()); + EXPECT_TRUE(textfield_->key_handled()); + EXPECT_STR_EQ("0123321456789", textfield_->text()); + EXPECT_EQ(8U, textfield_->GetCursorPosition()); + EXPECT_EQ(1, on_before_user_action_); + EXPECT_EQ(1, on_after_user_action_); + + input_method_->Clear(); + textfield_->SetReadOnly(true); + EXPECT_TRUE(input_method_->text_input_type_changed()); + EXPECT_FALSE(textfield_->GetTextInputClient()); + + textfield_->SetReadOnly(false); + input_method_->Clear(); + textfield_->SetPassword(true); + EXPECT_TRUE(input_method_->text_input_type_changed()); + EXPECT_TRUE(textfield_->GetTextInputClient()); +} + +TEST_F(NativeTextfieldViewsTest, UndoRedoTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + SendKeyEvent(ui::VKEY_A); + EXPECT_STR_EQ("a", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("a", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("a", textfield_->text()); + + // AppendText + textfield_->AppendText(ASCIIToUTF16("b")); + last_contents_.clear(); // AppendText doesn't call ContentsChanged. + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("a", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + + // SetText + SendKeyEvent(ui::VKEY_C); + // Undo'ing append moves the cursor to the end for now. + // no-op SetText won't add new edit. See TextfieldViewsModel::SetText + // description. + EXPECT_STR_EQ("abc", textfield_->text()); + textfield_->SetText(ASCIIToUTF16("abc")); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + textfield_->SetText(ASCIIToUTF16("123")); + textfield_->SetText(ASCIIToUTF16("123")); + EXPECT_STR_EQ("123", textfield_->text()); + SendKeyEvent(ui::VKEY_END, false, false); + SendKeyEvent(ui::VKEY_4, false, false); + EXPECT_STR_EQ("1234", textfield_->text()); + last_contents_.clear(); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("123", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + // the insert edit "c" and set edit "123" are merged to single edit, + // so text becomes "ab" after undo. + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("a", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("123", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("1234", textfield_->text()); + + // Undoing to the same text shouldn't call ContentsChanged. + SendKeyEvent(ui::VKEY_A, false, true); // select all + SendKeyEvent(ui::VKEY_A); + EXPECT_STR_EQ("a", textfield_->text()); + SendKeyEvent(ui::VKEY_B); + SendKeyEvent(ui::VKEY_C); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("1234", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + + // Delete/Backspace + SendKeyEvent(ui::VKEY_BACK); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_HOME); + SendKeyEvent(ui::VKEY_DELETE); + EXPECT_STR_EQ("b", textfield_->text()); + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent(ui::VKEY_DELETE); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("b", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("abc", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("ab", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("b", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("", textfield_->text()); + + // Insert + textfield_->SetText(ASCIIToUTF16("123")); + SendKeyEvent(ui::VKEY_HOME); + SendKeyEvent(ui::VKEY_INSERT); + SendKeyEvent(ui::VKEY_A); + EXPECT_STR_EQ("a23", textfield_->text()); + SendKeyEvent(ui::VKEY_B); + EXPECT_STR_EQ("ab3", textfield_->text()); + SendKeyEvent(ui::VKEY_Z, false, true); + EXPECT_STR_EQ("123", textfield_->text()); + SendKeyEvent(ui::VKEY_Y, false, true); + EXPECT_STR_EQ("ab3", textfield_->text()); +} + +TEST_F(NativeTextfieldViewsTest, TextCursorDisplayTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + // LTR-RTL string in LTR context. + SendKeyEvent('a'); + EXPECT_STR_EQ("a", textfield_->text()); + int x = GetCursorBounds().x(); + int prev_x = x; + + SendKeyEvent('b'); + EXPECT_STR_EQ("ab", textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_LT(prev_x, x); + prev_x = x; + + SendKeyEvent(0x05E1); + EXPECT_EQ(WideToUTF16(L"ab\x05E1"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + SendKeyEvent(0x05E2); + EXPECT_EQ(WideToUTF16(L"ab\x05E1\x5E2"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + // Clear text. + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent('\n'); + + // RTL-LTR string in LTR context. + SendKeyEvent(0x05E1); + EXPECT_EQ(WideToUTF16(L"\x05E1"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(GetDisplayRect().x(), x); + prev_x = x; + + SendKeyEvent(0x05E2); + EXPECT_EQ(WideToUTF16(L"\x05E1\x05E2"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + SendKeyEvent('a'); + EXPECT_EQ(WideToUTF16(L"\x05E1\x5E2"L"a"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_LT(prev_x, x); + prev_x = x; + + SendKeyEvent('b'); + EXPECT_EQ(WideToUTF16(L"\x05E1\x5E2"L"ab"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_LT(prev_x, x); +} + +TEST_F(NativeTextfieldViewsTest, TextCursorDisplayInRTLTest) { + std::string locale = l10n_util::GetApplicationLocale(""); + base::i18n::SetICUDefaultLocale("he"); + + InitTextfield(Textfield::STYLE_DEFAULT); + // LTR-RTL string in RTL context. + SendKeyEvent('a'); + EXPECT_STR_EQ("a", textfield_->text()); + int x = GetCursorBounds().x(); + EXPECT_EQ(GetDisplayRect().right() - 1, x); + int prev_x = x; + + SendKeyEvent('b'); + EXPECT_STR_EQ("ab", textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + SendKeyEvent(0x05E1); + EXPECT_EQ(WideToUTF16(L"ab\x05E1"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_GT(prev_x, x); + prev_x = x; + + SendKeyEvent(0x05E2); + EXPECT_EQ(WideToUTF16(L"ab\x05E1\x5E2"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_GT(prev_x, x); + + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent('\n'); + + // RTL-LTR string in RTL context. + SendKeyEvent(0x05E1); + EXPECT_EQ(WideToUTF16(L"\x05E1"), textfield_->text()); + x = GetCursorBounds().x(); + prev_x = x; + + SendKeyEvent(0x05E2); + EXPECT_EQ(WideToUTF16(L"\x05E1\x05E2"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_GT(prev_x, x); + prev_x = x; + + SendKeyEvent('a'); + EXPECT_EQ(WideToUTF16(L"\x05E1\x5E2"L"a"), textfield_->text()); + x = GetCursorBounds().x(); +#if defined(OS_WIN) + // In Windows, the text is always in LTR directionality even in RTL UI. + // TODO(xji): it should change if we fix the directionality in Window's + // NativeTextfieldViews + EXPECT_LT(prev_x, x); +#else + EXPECT_EQ(prev_x, x); +#endif + prev_x = x; + + SendKeyEvent('b'); + EXPECT_EQ(WideToUTF16(L"\x05E1\x5E2"L"ab"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + // Reset locale. + base::i18n::SetICUDefaultLocale(locale); +} + +TEST_F(NativeTextfieldViewsTest, HitInsideTextAreaTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(WideToUTF16(L"ab\x05E1\x5E2")); + std::vector<gfx::Rect> cursor_bounds; + + // Save each cursor bound. + gfx::SelectionModel sel(0, 0, gfx::SelectionModel::LEADING); + cursor_bounds.push_back(GetCursorBounds(sel)); + + sel = gfx::SelectionModel(1, 0, gfx::SelectionModel::TRAILING); + gfx::Rect bound = GetCursorBounds(sel); + sel = gfx::SelectionModel(1, 1, gfx::SelectionModel::LEADING); + EXPECT_EQ(bound.x(), GetCursorBounds(sel).x()); + cursor_bounds.push_back(bound); + + // Check that a cursor at the end of the Latin portion of the text is at the + // same position as a cursor placed at the end of the RTL Hebrew portion. + sel = gfx::SelectionModel(2, 1, gfx::SelectionModel::TRAILING); + bound = GetCursorBounds(sel); + sel = gfx::SelectionModel(4, 3, gfx::SelectionModel::TRAILING); + EXPECT_EQ(bound.x(), GetCursorBounds(sel).x()); + cursor_bounds.push_back(bound); + + sel = gfx::SelectionModel(3, 2, gfx::SelectionModel::TRAILING); + bound = GetCursorBounds(sel); + sel = gfx::SelectionModel(3, 3, gfx::SelectionModel::LEADING); + EXPECT_EQ(bound.x(), GetCursorBounds(sel).x()); + cursor_bounds.push_back(bound); + + sel = gfx::SelectionModel(2, 2, gfx::SelectionModel::LEADING); + bound = GetCursorBounds(sel); + sel = gfx::SelectionModel(4, 2, gfx::SelectionModel::LEADING); + EXPECT_EQ(bound.x(), GetCursorBounds(sel).x()); + cursor_bounds.push_back(bound); + + // Expected cursor position when clicking left and right of each character. + size_t cursor_pos_expected[] = {0, 1, 1, 2, 4, 3, 3, 2}; + + int index = 0; + for (int i = 0; i < static_cast<int>(cursor_bounds.size() - 1); ++i) { + int half_width = (cursor_bounds[i + 1].x() - cursor_bounds[i].x()) / 2; + MouseClick(cursor_bounds[i], half_width / 2); + EXPECT_EQ(cursor_pos_expected[index++], textfield_->GetCursorPosition()); + + // To avoid trigger double click. Not using sleep() since it takes longer + // for the test to run if using sleep(). + NonClientMouseClick(); + + MouseClick(cursor_bounds[i + 1], - (half_width / 2)); + EXPECT_EQ(cursor_pos_expected[index++], textfield_->GetCursorPosition()); + + NonClientMouseClick(); + } +} + +TEST_F(NativeTextfieldViewsTest, HitOutsideTextAreaTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + + // LTR-RTL string in LTR context. + textfield_->SetText(WideToUTF16(L"ab\x05E1\x5E2")); + + SendKeyEvent(ui::VKEY_HOME); + gfx::Rect bound = GetCursorBounds(); + MouseClick(bound, -10); + EXPECT_EQ(bound, GetCursorBounds()); + + SendKeyEvent(ui::VKEY_END); + bound = GetCursorBounds(); + MouseClick(bound, 10); + EXPECT_EQ(bound, GetCursorBounds()); + + NonClientMouseClick(); + + // RTL-LTR string in LTR context. + textfield_->SetText(WideToUTF16(L"\x05E1\x5E2"L"ab")); + + SendKeyEvent(ui::VKEY_HOME); + bound = GetCursorBounds(); +#if defined(OS_WIN) + MouseClick(bound, -10); +#else + MouseClick(bound, 10); +#endif + EXPECT_EQ(bound, GetCursorBounds()); + + SendKeyEvent(ui::VKEY_END); + bound = GetCursorBounds(); +#if defined(OS_WIN) + MouseClick(bound, 10); +#else + MouseClick(bound, -10); +#endif + EXPECT_EQ(bound, GetCursorBounds()); +} + +TEST_F(NativeTextfieldViewsTest, HitOutsideTextAreaInRTLTest) { + std::string locale = l10n_util::GetApplicationLocale(""); + base::i18n::SetICUDefaultLocale("he"); + + InitTextfield(Textfield::STYLE_DEFAULT); + + // RTL-LTR string in RTL context. + textfield_->SetText(WideToUTF16(L"\x05E1\x5E2"L"ab")); + SendKeyEvent(ui::VKEY_HOME); + gfx::Rect bound = GetCursorBounds(); + MouseClick(bound, 10); + EXPECT_EQ(bound, GetCursorBounds()); + + SendKeyEvent(ui::VKEY_END); + bound = GetCursorBounds(); + MouseClick(bound, -10); + EXPECT_EQ(bound, GetCursorBounds()); + + NonClientMouseClick(); + + // LTR-RTL string in RTL context. + textfield_->SetText(WideToUTF16(L"ab\x05E1\x5E2")); + SendKeyEvent(ui::VKEY_HOME); + bound = GetCursorBounds(); +#if defined(OS_WIN) + MouseClick(bound, 10); +#else + MouseClick(bound, -10); +#endif + EXPECT_EQ(bound, GetCursorBounds()); + + SendKeyEvent(ui::VKEY_END); + bound = GetCursorBounds(); +#if defined(OS_WIN) + MouseClick(bound, -10); +#else + MouseClick(bound, 10); +#endif + EXPECT_EQ(bound, GetCursorBounds()); + + // Reset locale. + base::i18n::SetICUDefaultLocale(locale); +} + +// This verifies that |bound| is contained by |display|. |bound|'s right edge +// must be less than |diaplay|'s right edge. +void OverflowCursorBoundTestVerifier(const gfx::Rect& display, + const gfx::Rect& bound) { + EXPECT_LE(display.x(), bound.x()); + EXPECT_GT(display.right(), bound.right()); + EXPECT_LE(display.y(), bound.y()); + EXPECT_GE(display.bottom(), bound.bottom()); +} + +TEST_F(NativeTextfieldViewsTest, OverflowTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + + string16 str; + for (int i = 0; i < 500; ++i) + SendKeyEvent('a'); + SendKeyEvent(kHebrewLetterSamekh); + gfx::Rect bound = GetCursorBounds(); + gfx::Rect display = GetDisplayRect(); + OverflowCursorBoundTestVerifier(display, bound); + + // Test mouse pointing. + MouseClick(bound, -1); + EXPECT_EQ(500U, textfield_->GetCursorPosition()); + + // Clear text. + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent('\n'); + + for (int i = 0; i < 500; ++i) + SendKeyEvent(kHebrewLetterSamekh); + SendKeyEvent('a'); + bound = GetCursorBounds(); + display = GetDisplayRect(); + OverflowCursorBoundTestVerifier(display, bound); + + MouseClick(bound, -1); + EXPECT_EQ(501U, textfield_->GetCursorPosition()); +} + +TEST_F(NativeTextfieldViewsTest, OverflowInRTLTest) { + std::string locale = l10n_util::GetApplicationLocale(""); + base::i18n::SetICUDefaultLocale("he"); + + InitTextfield(Textfield::STYLE_DEFAULT); + + string16 str; + for (int i = 0; i < 500; ++i) + SendKeyEvent('a'); + SendKeyEvent(kHebrewLetterSamekh); + gfx::Rect bound = GetCursorBounds(); + gfx::Rect display = GetDisplayRect(); + OverflowCursorBoundTestVerifier(display, bound); + + MouseClick(bound, 1); + EXPECT_EQ(501U, textfield_->GetCursorPosition()); + + // Clear text. + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent('\n'); + + for (int i = 0; i < 500; ++i) + SendKeyEvent(kHebrewLetterSamekh); + SendKeyEvent('a'); + bound = GetCursorBounds(); + display = GetDisplayRect(); + OverflowCursorBoundTestVerifier(display, bound); + + +#if !defined(OS_WIN) + // TODO(jennyz): NonClientMouseClick() does not work for os_win builds; + // see crbug.com/104150. The mouse click in the next test will be confused + // as a double click, which breaks the test. Disable the test until the + // issue is fixed. + NonClientMouseClick(); + + MouseClick(bound, 1); +#if defined(OS_WIN) + // In Windows, the text is always in LTR directionality even in RTL UI. + // TODO(xji): it should change if we fix the directionality in Window's + // NativeTextfieldViews + EXPECT_EQ(0U, textfield_->GetCursorPosition()); +#else + EXPECT_EQ(500U, textfield_->GetCursorPosition()); +#endif +#endif // !defined(OS_WIN) + + // Reset locale. + base::i18n::SetICUDefaultLocale(locale); +} + +} // namespace views diff --git a/ui/views/controls/textfield/native_textfield_wayland.cc b/ui/views/controls/textfield/native_textfield_wayland.cc new file mode 100644 index 0000000..63d02c7 --- /dev/null +++ b/ui/views/controls/textfield/native_textfield_wayland.cc @@ -0,0 +1,20 @@ +// 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 "base/logging.h" +#include "ui/views/controls/textfield/native_textfield_views.h" +#include "ui/views/controls/textfield/textfield.h" + +namespace views { + +/////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldWrapper: + +// static +NativeTextfieldWrapper* NativeTextfieldWrapper::CreateWrapper( + Textfield* field) { + return new NativeTextfieldViews(field); +} + +} // namespace views diff --git a/ui/views/controls/textfield/native_textfield_win.cc b/ui/views/controls/textfield/native_textfield_win.cc new file mode 100644 index 0000000..0741fd6 --- /dev/null +++ b/ui/views/controls/textfield/native_textfield_win.cc @@ -0,0 +1,1168 @@ +// 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 "ui/views/controls/textfield/native_textfield_win.h" + +#include <algorithm> + +#include "base/i18n/case_conversion.h" +#include "base/i18n/rtl.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "base/win/windows_version.h" +#include "grit/ui_strings.h" +#include "skia/ext/skia_utils_win.h" +#include "ui/base/accessibility/accessible_view_state.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/clipboard/scoped_clipboard_writer.h" +#include "ui/base/keycodes/keyboard_code_conversion_win.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/l10n/l10n_util_win.h" +#include "ui/base/range/range.h" +#include "ui/base/win/mouse_wheel_util.h" +#include "ui/gfx/native_theme_win.h" +#include "ui/views/controls/native/native_view_host.h" +#include "ui/views/controls/textfield/native_textfield_views.h" +#include "ui/views/controls/textfield/textfield.h" +#include "ui/views/controls/textfield/textfield_controller.h" +#include "ui/views/focus/focus_manager.h" +#include "ui/views/widget/widget.h" +#include "views/controls/label.h" +#include "views/controls/menu/menu_2.h" +#include "views/controls/menu/menu_win.h" +#include "views/metrics.h" +#include "views/views_delegate.h" + +namespace views { + +/////////////////////////////////////////////////////////////////////////////// +// Helper classes + +NativeTextfieldWin::ScopedFreeze::ScopedFreeze(NativeTextfieldWin* edit, + ITextDocument* text_object_model) + : edit_(edit), + text_object_model_(text_object_model) { + // Freeze the screen. + if (text_object_model_) { + long count; + text_object_model_->Freeze(&count); + } +} + +NativeTextfieldWin::ScopedFreeze::~ScopedFreeze() { + // Unfreeze the screen. + if (text_object_model_) { + long count; + text_object_model_->Unfreeze(&count); + if (count == 0) { + // We need to UpdateWindow() here instead of InvalidateRect() because, as + // far as I can tell, the edit likes to synchronously erase its background + // when unfreezing, thus requiring us to synchronously redraw if we don't + // want flicker. + edit_->UpdateWindow(); + } + } +} + +NativeTextfieldWin::ScopedSuspendUndo::ScopedSuspendUndo( + ITextDocument* text_object_model) + : text_object_model_(text_object_model) { + // Suspend Undo processing. + if (text_object_model_) + text_object_model_->Undo(tomSuspend, NULL); +} + +NativeTextfieldWin::ScopedSuspendUndo::~ScopedSuspendUndo() { + // Resume Undo processing. + if (text_object_model_) + text_object_model_->Undo(tomResume, NULL); +} + +/////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldWin + +bool NativeTextfieldWin::did_load_library_ = false; + +NativeTextfieldWin::NativeTextfieldWin(Textfield* textfield) + : textfield_(textfield), + tracking_double_click_(false), + double_click_time_(0), + can_discard_mousemove_(false), + contains_mouse_(false), + ime_discard_composition_(false), + ime_composition_start_(0), + ime_composition_length_(0), + container_view_(new NativeViewHost), + bg_color_(0) { + if (!did_load_library_) + did_load_library_ = !!LoadLibrary(L"riched20.dll"); + + DWORD style = kDefaultEditStyle | ES_AUTOHSCROLL; + if (textfield_->style() & Textfield::STYLE_PASSWORD) + style |= ES_PASSWORD; + + if (textfield_->read_only()) + style |= ES_READONLY; + + // Make sure we apply RTL related extended window styles if necessary. + DWORD ex_style = l10n_util::GetExtendedStyles(); + + RECT r = {0, 0, textfield_->width(), textfield_->height()}; + Create(textfield_->GetWidget()->GetNativeView(), r, NULL, style, ex_style); + + if (textfield_->style() & Textfield::STYLE_LOWERCASE) { + DCHECK((textfield_->style() & Textfield::STYLE_PASSWORD) == 0); + SetEditStyle(SES_LOWERCASE, SES_LOWERCASE); + } + + // Set up the text_object_model_. + base::win::ScopedComPtr<IRichEditOle, &IID_IRichEditOle> ole_interface; + ole_interface.Attach(GetOleInterface()); + if (ole_interface) + text_object_model_.QueryFrom(ole_interface); + + InitializeAccessibilityInfo(); +} + +NativeTextfieldWin::~NativeTextfieldWin() { + if (IsWindow()) + DestroyWindow(); +} + +// static +bool NativeTextfieldWin::IsDoubleClick(const POINT& origin, + const POINT& current, + DWORD elapsed_time) { + // The CXDOUBLECLK and CYDOUBLECLK system metrics describe the width and + // height of a rectangle around the origin position, inside of which clicks + // within the double click time are considered double clicks. + return (elapsed_time <= GetDoubleClickTime()) && + (abs(current.x - origin.x) <= (GetSystemMetrics(SM_CXDOUBLECLK) / 2)) && + (abs(current.y - origin.y) <= (GetSystemMetrics(SM_CYDOUBLECLK) / 2)); +} + +// static +bool NativeTextfieldWin::IsNumPadDigit(int key_code, bool extended_key) { + if (key_code >= VK_NUMPAD0 && key_code <= VK_NUMPAD9) + return true; + + // Check for num pad keys without NumLock. + // Note: there is no easy way to know if a the key that was pressed comes from + // the num pad or the rest of the keyboard. Investigating how + // TranslateMessage() generates the WM_KEYCHAR from an + // ALT + <NumPad sequences> it appears it looks at the extended key flag + // (which is on if the key pressed comes from one of the 3 clusters to + // the left of the numeric keypad). So we use it as well. + return !extended_key && + ((key_code >= VK_PRIOR && key_code <= VK_DOWN) || // All keys but 5 + // and 0. + (key_code == VK_CLEAR) || // Key 5. + (key_code == VK_INSERT)); // Key 0. +} + +void NativeTextfieldWin::AttachHack() { + // See the code in textfield.cc that calls this for why this is here. + container_view_->set_focus_view(textfield_); + container_view_->Attach(m_hWnd); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldWin, NativeTextfieldWrapper implementation: + +string16 NativeTextfieldWin::GetText() const { + int len = GetTextLength() + 1; + if (len <= 1) + return string16(); + + string16 str; + GetWindowText(WriteInto(&str, len), len); + // The text get from GetWindowText() might be wrapped with explicit bidi + // control characters. Refer to UpdateText() for detail. Without such + // wrapping, in RTL chrome, a pure LTR string ending with parenthesis will + // not be displayed correctly in a textfield. For example, "Yahoo!" will be + // displayed as "!Yahoo", and "Google (by default)" will be displayed as + // "(Google (by default". + return base::i18n::StripWrappingBidiControlCharacters(WideToUTF16(str)); +} + +void NativeTextfieldWin::UpdateText() { + string16 text = textfield_->text(); + // Adjusting the string direction before setting the text in order to make + // sure both RTL and LTR strings are displayed properly. + base::i18n::AdjustStringForLocaleDirection(&text); + if (textfield_->style() & Textfield::STYLE_LOWERCASE) + text = base::i18n::ToLower(text); + SetWindowText(text.c_str()); + UpdateAccessibleValue(text); +} + +void NativeTextfieldWin::AppendText(const string16& text) { + int text_length = GetWindowTextLength(); + ::SendMessage(m_hWnd, TBM_SETSEL, true, MAKELPARAM(text_length, text_length)); + ::SendMessage(m_hWnd, EM_REPLACESEL, false, + reinterpret_cast<LPARAM>(text.c_str())); +} + +string16 NativeTextfieldWin::GetSelectedText() const { + // Figure out the length of the selection. + CHARRANGE sel; + GetSel(sel); + if (sel.cpMin == sel.cpMax) // GetSelText() crashes on NULL input. + return string16(); + + // Grab the selected text. + string16 str; + GetSelText(WriteInto(&str, sel.cpMax - sel.cpMin + 1)); + return str; +} + +void NativeTextfieldWin::SelectAll() { + // Select from the end to the front so that the first part of the text is + // always visible. + SetSel(GetTextLength(), 0); +} + +void NativeTextfieldWin::ClearSelection() { + SetSel(GetTextLength(), GetTextLength()); +} + +void NativeTextfieldWin::UpdateBorder() { + SetWindowPos(NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_FRAMECHANGED | SWP_NOACTIVATE | + SWP_NOOWNERZORDER | SWP_NOSIZE); +} + +void NativeTextfieldWin::UpdateTextColor() { + CHARFORMAT cf = {0}; + cf.dwMask = CFM_COLOR; + cf.crTextColor = textfield_->use_default_text_color() ? + GetSysColor(textfield_->read_only() ? COLOR_GRAYTEXT : COLOR_WINDOWTEXT) : + skia::SkColorToCOLORREF(textfield_->text_color()); + CRichEditCtrl::SetDefaultCharFormat(cf); +} + +void NativeTextfieldWin::UpdateBackgroundColor() { + if (!textfield_->use_default_background_color()) { + bg_color_ = skia::SkColorToCOLORREF(textfield_->background_color()); + } else { + bg_color_ = GetSysColor(textfield_->read_only() ? COLOR_3DFACE + : COLOR_WINDOW); + } + CRichEditCtrl::SetBackgroundColor(bg_color_); +} + +void NativeTextfieldWin::UpdateReadOnly() { + SendMessage(m_hWnd, EM_SETREADONLY, textfield_->read_only(), 0); + UpdateAccessibleState(STATE_SYSTEM_READONLY, textfield_->read_only()); +} + +void NativeTextfieldWin::UpdateFont() { + SendMessage(m_hWnd, WM_SETFONT, + reinterpret_cast<WPARAM>(textfield_->font().GetNativeFont()), + TRUE); + // Setting the font blows away any text color we've set, so reset it. + UpdateTextColor(); +} + +void NativeTextfieldWin::UpdateIsPassword() { + // TODO: Need to implement for Windows. + UpdateAccessibleState(STATE_SYSTEM_PROTECTED, textfield_->IsPassword()); +} + +void NativeTextfieldWin::UpdateEnabled() { + SendMessage(m_hWnd, WM_ENABLE, textfield_->IsEnabled(), 0); + UpdateAccessibleState(STATE_SYSTEM_UNAVAILABLE, !textfield_->IsEnabled()); +} + +gfx::Insets NativeTextfieldWin::CalculateInsets() { + // NOTE: One would think GetThemeMargins would return the insets we should + // use, but it doesn't. The margins returned by GetThemeMargins are always + // 0. + + // This appears to be the insets used by Windows. + return gfx::Insets(3, 3, 3, 3); +} + +void NativeTextfieldWin::UpdateHorizontalMargins() { + int left, right; + if (!textfield_->GetHorizontalMargins(&left, &right)) + return; + + // SendMessage expects the two values to be packed into one using MAKELONG + // so we truncate to 16 bits if necessary. + SendMessage(m_hWnd, EM_SETMARGINS, + EC_LEFTMARGIN | EC_RIGHTMARGIN, + MAKELONG(left & 0xFFFF, right & 0xFFFF)); +} + +void NativeTextfieldWin::UpdateVerticalMargins() { + int top, bottom; + if (!textfield_->GetVerticalMargins(&top, &bottom)) + return; + + if (top == 0 && bottom == 0) { + // Do nothing, default margins are 0 already. + return; + } + // Non-zero margins case. + NOTIMPLEMENTED(); +} + +bool NativeTextfieldWin::SetFocus() { + // Focus the associated HWND. + //container_view_->Focus(); + ::SetFocus(m_hWnd); + return true; +} + +View* NativeTextfieldWin::GetView() { + return container_view_; +} + +gfx::NativeView NativeTextfieldWin::GetTestingHandle() const { + return m_hWnd; +} + +bool NativeTextfieldWin::IsIMEComposing() const { + // Retrieve the length of the composition string to check if an IME is + // composing text. (If this length is > 0 then an IME is being used to compose + // text.) + HIMC imm_context = ImmGetContext(m_hWnd); + if (!imm_context) + return false; + + const int composition_size = ImmGetCompositionString(imm_context, GCS_COMPSTR, + NULL, 0); + ImmReleaseContext(m_hWnd, imm_context); + return composition_size > 0; +} + +void NativeTextfieldWin::GetSelectedRange(ui::Range* range) const { + NOTREACHED(); +} + +void NativeTextfieldWin::SelectRange(const ui::Range& range) { + NOTREACHED(); +} + +void NativeTextfieldWin::GetSelectionModel(gfx::SelectionModel* sel) const { + NOTREACHED(); +} + +void NativeTextfieldWin::SelectSelectionModel(const gfx::SelectionModel& sel) { + NOTREACHED(); +} + +size_t NativeTextfieldWin::GetCursorPosition() const { + NOTREACHED(); + return 0U; +} + +bool NativeTextfieldWin::HandleKeyPressed(const views::KeyEvent& event) { + return false; +} + +bool NativeTextfieldWin::HandleKeyReleased(const views::KeyEvent& event) { + return false; +} + +void NativeTextfieldWin::HandleFocus() { +} + +void NativeTextfieldWin::HandleBlur() { +} + +ui::TextInputClient* NativeTextfieldWin::GetTextInputClient() { + return NULL; +} + +void NativeTextfieldWin::ApplyStyleRange(const gfx::StyleRange& style) { + NOTREACHED(); +} + +void NativeTextfieldWin::ApplyDefaultStyle() { + NOTREACHED(); +} + +void NativeTextfieldWin::ClearEditHistory() { + NOTREACHED(); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldWin, ui::SimpleMenuModel::Delegate implementation: + +bool NativeTextfieldWin::IsCommandIdChecked(int command_id) const { + return false; +} + +bool NativeTextfieldWin::IsCommandIdEnabled(int command_id) const { + switch (command_id) { + case IDS_APP_UNDO: return !textfield_->read_only() && !!CanUndo(); + case IDS_APP_CUT: return !textfield_->read_only() && + !textfield_->IsPassword() && !!CanCut(); + case IDS_APP_COPY: return !!CanCopy() && !textfield_->IsPassword(); + case IDS_APP_PASTE: return !textfield_->read_only() && !!CanPaste(); + case IDS_APP_SELECT_ALL: return !!CanSelectAll(); + default: NOTREACHED(); + return false; + } +} + +bool NativeTextfieldWin::GetAcceleratorForCommandId(int command_id, + ui::Accelerator* accelerator) { + // The standard Ctrl-X, Ctrl-V and Ctrl-C are not defined as accelerators + // anywhere so we need to check for them explicitly here. + switch (command_id) { + case IDS_APP_CUT: + *accelerator = ui::Accelerator(ui::VKEY_X, false, true, false); + return true; + case IDS_APP_COPY: + *accelerator = ui::Accelerator(ui::VKEY_C, false, true, false); + return true; + case IDS_APP_PASTE: + *accelerator = ui::Accelerator(ui::VKEY_V, false, true, false); + return true; + } + return container_view_->GetWidget()->GetAccelerator(command_id, accelerator); +} + +void NativeTextfieldWin::ExecuteCommand(int command_id) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + switch (command_id) { + case IDS_APP_UNDO: Undo(); break; + case IDS_APP_CUT: Cut(); break; + case IDS_APP_COPY: Copy(); break; + case IDS_APP_PASTE: Paste(); break; + case IDS_APP_SELECT_ALL: SelectAll(); break; + default: NOTREACHED(); break; + } + OnAfterPossibleChange(true); +} + +void NativeTextfieldWin::InitializeAccessibilityInfo() { + // Set the accessible state. + accessibility_state_ = 0; + + base::win::ScopedComPtr<IAccPropServices> pAccPropServices; + HRESULT hr = CoCreateInstance(CLSID_AccPropServices, NULL, CLSCTX_SERVER, + IID_IAccPropServices, reinterpret_cast<void**>(&pAccPropServices)); + if (!SUCCEEDED(hr)) + return; + + VARIANT var; + + // Set the accessible role. + var.vt = VT_I4; + var.lVal = ROLE_SYSTEM_TEXT; + hr = pAccPropServices->SetHwndProp(m_hWnd, OBJID_CLIENT, + CHILDID_SELF, PROPID_ACC_ROLE, var); + + // Set the accessible name by getting the label text. + View* parent = textfield_->parent(); + int label_index = parent->GetIndexOf(textfield_) - 1; + if (label_index >= 0) { + // Try to find the name of this text field. + // We expect it to be a Label preceeding this view (if it exists). + string16 name; + View* label_view = parent->child_at(label_index); + if (label_view->GetClassName() == Label::kViewClassName) { + ui::AccessibleViewState state; + label_view->GetAccessibleState(&state); + hr = pAccPropServices->SetHwndPropStr(m_hWnd, OBJID_CLIENT, + CHILDID_SELF, PROPID_ACC_NAME, state.name.c_str()); + } + } +} + +void NativeTextfieldWin::UpdateAccessibleState(uint32 state_flag, + bool set_value) { + base::win::ScopedComPtr<IAccPropServices> pAccPropServices; + HRESULT hr = CoCreateInstance(CLSID_AccPropServices, NULL, CLSCTX_SERVER, + IID_IAccPropServices, reinterpret_cast<void**>(&pAccPropServices)); + if (!SUCCEEDED(hr)) + return; + + VARIANT var; + var.vt = VT_I4; + var.lVal = set_value ? accessibility_state_ | state_flag + : accessibility_state_ & ~state_flag; + hr = pAccPropServices->SetHwndProp(m_hWnd, OBJID_CLIENT, + CHILDID_SELF, PROPID_ACC_STATE, var); + + ::NotifyWinEvent(EVENT_OBJECT_STATECHANGE, m_hWnd, OBJID_CLIENT, + CHILDID_SELF); +} + +void NativeTextfieldWin::UpdateAccessibleValue(const string16& value) { + base::win::ScopedComPtr<IAccPropServices> pAccPropServices; + HRESULT hr = CoCreateInstance(CLSID_AccPropServices, NULL, CLSCTX_SERVER, + IID_IAccPropServices, reinterpret_cast<void**>(&pAccPropServices)); + if (!SUCCEEDED(hr)) + return; + + hr = pAccPropServices->SetHwndPropStr(m_hWnd, OBJID_CLIENT, + CHILDID_SELF, PROPID_ACC_VALUE, value.c_str()); + + ::NotifyWinEvent(EVENT_OBJECT_VALUECHANGE, m_hWnd, OBJID_CLIENT, + CHILDID_SELF); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldWin, private: + +void NativeTextfieldWin::OnChar(TCHAR ch, UINT repeat_count, UINT flags) { + HandleKeystroke(); +} + +void NativeTextfieldWin::OnContextMenu(HWND window, const POINT& point) { + POINT p(point); + if (point.x == -1 || point.y == -1) { + GetCaretPos(&p); + MapWindowPoints(HWND_DESKTOP, &p, 1); + } + BuildContextMenu(); + context_menu_->RunContextMenuAt(gfx::Point(p)); +} + +void NativeTextfieldWin::OnCopy() { + if (textfield_->IsPassword()) + return; + + const string16 text(GetSelectedText()); + if (!text.empty() && ViewsDelegate::views_delegate) { + ui::ScopedClipboardWriter scw( + ViewsDelegate::views_delegate->GetClipboard()); + scw.WriteText(text); + } +} + +void NativeTextfieldWin::OnCut() { + if (textfield_->read_only() || textfield_->IsPassword()) + return; + + OnCopy(); + + // This replace selection will have no effect (even on the undo stack) if the + // current selection is empty. + ReplaceSel(L"", true); +} + +LRESULT NativeTextfieldWin::OnImeChar(UINT message, + WPARAM wparam, + LPARAM lparam) { + // http://crbug.com/7707: a rich-edit control may crash when it receives a + // WM_IME_CHAR message while it is processing a WM_IME_COMPOSITION message. + // Since view controls don't need WM_IME_CHAR messages, we prevent WM_IME_CHAR + // messages from being dispatched to view controls via the CallWindowProc() + // call. + return 0; +} + +LRESULT NativeTextfieldWin::OnImeStartComposition(UINT message, + WPARAM wparam, + LPARAM lparam) { + // Users may press alt+shift or control+shift keys to change their keyboard + // layouts. So, we retrieve the input locale identifier everytime we start + // an IME composition. + int language_id = PRIMARYLANGID(GetKeyboardLayout(0)); + ime_discard_composition_ = + language_id == LANG_JAPANESE || language_id == LANG_CHINESE; + ime_composition_start_ = 0; + ime_composition_length_ = 0; + + return DefWindowProc(message, wparam, lparam); +} + +LRESULT NativeTextfieldWin::OnImeComposition(UINT message, + WPARAM wparam, + LPARAM lparam) { + text_before_change_.clear(); + LRESULT result = DefWindowProc(message, wparam, lparam); + + ime_composition_start_ = 0; + ime_composition_length_ = 0; + if (ime_discard_composition_) { + // Call IMM32 functions to retrieve the position and the length of the + // ongoing composition string and notify the OnAfterPossibleChange() + // function that it should discard the composition string from a search + // string. We should not call IMM32 functions in the function because it + // is called when an IME is not composing a string. + HIMC imm_context = ImmGetContext(m_hWnd); + if (imm_context) { + CHARRANGE selection; + GetSel(selection); + const int cursor_position = + ImmGetCompositionString(imm_context, GCS_CURSORPOS, NULL, 0); + if (cursor_position >= 0) + ime_composition_start_ = selection.cpMin - cursor_position; + + const int composition_size = + ImmGetCompositionString(imm_context, GCS_COMPSTR, NULL, 0); + if (composition_size >= 0) + ime_composition_length_ = composition_size / sizeof(wchar_t); + + ImmReleaseContext(m_hWnd, imm_context); + } + } + + // If we allow OnAfterPossibleChange() to redraw the text, it will do this by + // setting the edit's text directly, which can cancel the current IME + // composition or cause other adverse affects. So we set |should_redraw_text| + // to false. + OnAfterPossibleChange(false); + return result; +} + +LRESULT NativeTextfieldWin::OnImeEndComposition(UINT message, + WPARAM wparam, + LPARAM lparam) { + // Bug 11863: Korean IMEs send a WM_IME_ENDCOMPOSITION message without + // sending any WM_IME_COMPOSITION messages when a user deletes all + // composition characters, i.e. a composition string becomes empty. To handle + // this case, we need to update the find results when a composition is + // finished or canceled. + textfield_->SyncText(); + return DefWindowProc(message, wparam, lparam); +} + +void NativeTextfieldWin::OnKeyDown(TCHAR key, UINT repeat_count, UINT flags) { + // NOTE: Annoyingly, ctrl-alt-<key> generates WM_KEYDOWN rather than + // WM_SYSKEYDOWN, so we need to check (flags & KF_ALTDOWN) in various places + // in this function even with a WM_SYSKEYDOWN handler. + + switch (key) { + + // Ignore Return + case VK_RETURN: + return; + + // Hijacking Editing Commands + // + // We hijack the keyboard short-cuts for Cut, Copy, and Paste here so that + // they go through our clipboard routines. This allows us to be smarter + // about how we interact with the clipboard and avoid bugs in the + // CRichEditCtrl. If we didn't hijack here, the edit control would handle + // these internally with sending the WM_CUT, WM_COPY, or WM_PASTE messages. + // + // Cut: Shift-Delete and Ctrl-x are treated as cut. Ctrl-Shift-Delete and + // Ctrl-Shift-x are not treated as cut even though the underlying + // CRichTextEdit would treat them as such. + // Copy: Ctrl-c is treated as copy. Shift-Ctrl-c is not. + // Paste: Shift-Insert and Ctrl-v are tread as paste. Ctrl-Shift-Insert and + // Ctrl-Shift-v are not. + // + // This behavior matches most, but not all Windows programs, and largely + // conforms to what users expect. + + case VK_DELETE: + case 'X': + if ((flags & KF_ALTDOWN) || + (GetKeyState((key == 'X') ? VK_CONTROL : VK_SHIFT) >= 0)) + break; + if (GetKeyState((key == 'X') ? VK_SHIFT : VK_CONTROL) >= 0) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + Cut(); + OnAfterPossibleChange(true); + } + return; + + case 'C': + if ((flags & KF_ALTDOWN) || (GetKeyState(VK_CONTROL) >= 0)) + break; + if (GetKeyState(VK_SHIFT) >= 0) + Copy(); + return; + + case VK_INSERT: + // Ignore insert by itself, so we don't turn overtype mode on/off. + if (!(flags & KF_ALTDOWN) && (GetKeyState(VK_SHIFT) >= 0) && + (GetKeyState(VK_CONTROL) >= 0)) + return; + // Fall through to the next case (ie. Shift-Insert == Ctrl-V). + case 'V': + if ((flags & KF_ALTDOWN) || + (GetKeyState((key == 'V') ? VK_CONTROL : VK_SHIFT) >= 0)) + break; + if (GetKeyState((key == 'V') ? VK_SHIFT : VK_CONTROL) >= 0) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + Paste(); + OnAfterPossibleChange(true); + } + return; + + case 0xbb: // Ctrl-'='. Triggers subscripting, even in plain text mode. + // We don't use VK_OEM_PLUS in case the macro isn't defined. + // (e.g., we don't have this symbol in embeded environment). + return; + + case VK_PROCESSKEY: + // This key event is consumed by an IME. + // We ignore this event because an IME sends WM_IME_COMPOSITION messages + // when it updates the CRichEditCtrl text. + return; + } + + // CRichEditCtrl changes its text on WM_KEYDOWN instead of WM_CHAR for many + // different keys (backspace, ctrl-v, ...), so we call this in both cases. + HandleKeystroke(); +} + +void NativeTextfieldWin::OnLButtonDblClk(UINT keys, const CPoint& point) { + // Save the double click info for later triple-click detection. + tracking_double_click_ = true; + double_click_point_ = point; + double_click_time_ = GetCurrentMessage()->time; + + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + DefWindowProc(WM_LBUTTONDBLCLK, keys, + MAKELPARAM(ClipXCoordToVisibleText(point.x, false), point.y)); + OnAfterPossibleChange(true); +} + +void NativeTextfieldWin::OnLButtonDown(UINT keys, const CPoint& point) { + // Check for triple click, then reset tracker. Should be safe to subtract + // double_click_time_ from the current message's time even if the timer has + // wrapped in between. + const bool is_triple_click = tracking_double_click_ && + IsDoubleClick(double_click_point_, point, + GetCurrentMessage()->time - double_click_time_); + tracking_double_click_ = false; + + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + DefWindowProc(WM_LBUTTONDOWN, keys, + MAKELPARAM(ClipXCoordToVisibleText(point.x, is_triple_click), + point.y)); + OnAfterPossibleChange(true); +} + +void NativeTextfieldWin::OnLButtonUp(UINT keys, const CPoint& point) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + DefWindowProc(WM_LBUTTONUP, keys, + MAKELPARAM(ClipXCoordToVisibleText(point.x, false), point.y)); + OnAfterPossibleChange(true); +} + +void NativeTextfieldWin::OnMouseLeave() { + SetContainsMouse(false); +} + +LRESULT NativeTextfieldWin::OnMouseWheel(UINT message, + WPARAM w_param, + LPARAM l_param) { + // Reroute the mouse-wheel to the window under the mouse pointer if + // applicable. + if (ui::RerouteMouseWheel(m_hWnd, w_param, l_param)) + return 0; + return DefWindowProc(message, w_param, l_param); +} + +void NativeTextfieldWin::OnMouseMove(UINT keys, const CPoint& point) { + SetContainsMouse(true); + // Clamp the selection to the visible text so the user can't drag to select + // the "phantom newline". In theory we could achieve this by clipping the X + // coordinate, but in practice the edit seems to behave nondeterministically + // with similar sequences of clipped input coordinates fed to it. Maybe it's + // reading the mouse cursor position directly? + // + // This solution has a minor visual flaw, however: if there's a visible + // cursor at the edge of the text (only true when there's no selection), + // dragging the mouse around outside that edge repaints the cursor on every + // WM_MOUSEMOVE instead of allowing it to blink normally. To fix this, we + // special-case this exact case and discard the WM_MOUSEMOVE messages instead + // of passing them along. + // + // But even this solution has a flaw! (Argh.) In the case where the user + // has a selection that starts at the edge of the edit, and proceeds to the + // middle of the edit, and the user is dragging back past the start edge to + // remove the selection, there's a redraw problem where the change between + // having the last few bits of text still selected and having nothing + // selected can be slow to repaint (which feels noticeably strange). This + // occurs if you only let the edit receive a single WM_MOUSEMOVE past the + // edge of the text. I think on each WM_MOUSEMOVE the edit is repainting its + // previous state, then updating its internal variables to the new state but + // not repainting. To fix this, we allow one more WM_MOUSEMOVE through after + // the selection has supposedly been shrunk to nothing; this makes the edit + // redraw the selection quickly so it feels smooth. + CHARRANGE selection; + GetSel(selection); + const bool possibly_can_discard_mousemove = + (selection.cpMin == selection.cpMax) && + (((selection.cpMin == 0) && + (ClipXCoordToVisibleText(point.x, false) > point.x)) || + ((selection.cpMin == GetTextLength()) && + (ClipXCoordToVisibleText(point.x, false) < point.x))); + if (!can_discard_mousemove_ || !possibly_can_discard_mousemove) { + can_discard_mousemove_ = possibly_can_discard_mousemove; + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + // Force the Y coordinate to the center of the clip rect. The edit + // behaves strangely when the cursor is dragged vertically: if the cursor + // is in the middle of the text, drags inside the clip rect do nothing, + // and drags outside the clip rect act as if the cursor jumped to the + // left edge of the text. When the cursor is at the right edge, drags of + // just a few pixels vertically end up selecting the "phantom newline"... + // sometimes. + RECT r; + GetRect(&r); + DefWindowProc(WM_MOUSEMOVE, keys, + MAKELPARAM(point.x, (r.bottom - r.top) / 2)); + OnAfterPossibleChange(true); + } +} + +int NativeTextfieldWin::OnNCCalcSize(BOOL w_param, LPARAM l_param) { + content_insets_.Set(0, 0, 0, 0); + if (textfield_->draw_border()) + content_insets_ = CalculateInsets(); + if (w_param) { + NCCALCSIZE_PARAMS* nc_params = + reinterpret_cast<NCCALCSIZE_PARAMS*>(l_param); + nc_params->rgrc[0].left += content_insets_.left(); + nc_params->rgrc[0].right -= content_insets_.right(); + nc_params->rgrc[0].top += content_insets_.top(); + nc_params->rgrc[0].bottom -= content_insets_.bottom(); + } else { + RECT* rect = reinterpret_cast<RECT*>(l_param); + rect->left += content_insets_.left(); + rect->right -= content_insets_.right(); + rect->top += content_insets_.top(); + rect->bottom -= content_insets_.bottom(); + } + return 0; +} + +void NativeTextfieldWin::OnNCPaint(HRGN region) { + if (!textfield_->draw_border()) + return; + + HDC hdc = GetWindowDC(); + + CRect window_rect; + GetWindowRect(&window_rect); + // Convert to be relative to 0x0. + window_rect.MoveToXY(0, 0); + + ExcludeClipRect(hdc, + window_rect.left + content_insets_.left(), + window_rect.top + content_insets_.top(), + window_rect.right - content_insets_.right(), + window_rect.bottom - content_insets_.bottom()); + + HBRUSH brush = CreateSolidBrush(bg_color_); + FillRect(hdc, &window_rect, brush); + DeleteObject(brush); + + int part; + int state; + + if (base::win::GetVersion() < base::win::VERSION_VISTA) { + part = EP_EDITTEXT; + + if (!textfield_->IsEnabled()) + state = ETS_DISABLED; + else if (textfield_->read_only()) + state = ETS_READONLY; + else if (!contains_mouse_) + state = ETS_NORMAL; + else + state = ETS_HOT; + } else { + part = EP_EDITBORDER_HVSCROLL; + + if (!textfield_->IsEnabled()) + state = EPSHV_DISABLED; + else if (GetFocus() == m_hWnd) + state = EPSHV_FOCUSED; + else if (contains_mouse_) + state = EPSHV_HOT; + else + state = EPSHV_NORMAL; + // Vista doesn't appear to have a unique state for readonly. + } + + int classic_state = + (!textfield_->IsEnabled() || textfield_->read_only()) ? DFCS_INACTIVE : 0; + + gfx::NativeThemeWin::instance()->PaintTextField(hdc, part, state, + classic_state, &window_rect, + bg_color_, false, true); + + // NOTE: I tried checking the transparent property of the theme and invoking + // drawParentBackground, but it didn't seem to make a difference. + + ReleaseDC(hdc); +} + +void NativeTextfieldWin::OnNonLButtonDown(UINT keys, const CPoint& point) { + // Interestingly, the edit doesn't seem to cancel triple clicking when the + // x-buttons (which usually means "thumb buttons") are pressed, so we only + // call this for M and R down. + tracking_double_click_ = false; + SetMsgHandled(false); +} + +void NativeTextfieldWin::OnPaste() { + if (textfield_->read_only() || !ViewsDelegate::views_delegate) + return; + + ui::Clipboard* clipboard = ViewsDelegate::views_delegate->GetClipboard(); + if (!clipboard->IsFormatAvailable(ui::Clipboard::GetPlainTextWFormatType(), + ui::Clipboard::BUFFER_STANDARD)) + return; + + string16 clipboard_str; + clipboard->ReadText(ui::Clipboard::BUFFER_STANDARD, &clipboard_str); + if (!clipboard_str.empty()) { + string16 collapsed(CollapseWhitespace(clipboard_str, false)); + if (textfield_->style() & Textfield::STYLE_LOWERCASE) + collapsed = base::i18n::ToLower(collapsed); + // Force a Paste operation to trigger ContentsChanged, even if identical + // contents are pasted into the text box. See http://crbug.com/79002 + ReplaceSel(L"", false); + textfield_->SyncText(); + text_before_change_.clear(); + ReplaceSel(collapsed.c_str(), true); + } +} + +void NativeTextfieldWin::OnSetFocus(HWND hwnd) { + SetMsgHandled(FALSE); // We still want the default processing of the message. + + views::FocusManager* focus_manager = textfield_->GetFocusManager(); + if (!focus_manager) { + NOTREACHED(); + return; + } + focus_manager->SetFocusedView(textfield_); +} + +void NativeTextfieldWin::OnSysChar(TCHAR ch, UINT repeat_count, UINT flags) { + // Nearly all alt-<xxx> combos result in beeping rather than doing something + // useful, so we discard most. Exceptions: + // * ctrl-alt-<xxx>, which is sometimes important, generates WM_CHAR instead + // of WM_SYSCHAR, so it doesn't need to be handled here. + // * alt-space gets translated by the default WM_SYSCHAR handler to a + // WM_SYSCOMMAND to open the application context menu, so we need to allow + // it through. + if (ch == VK_SPACE) + SetMsgHandled(false); +} + +void NativeTextfieldWin::HandleKeystroke() { + const MSG* msg = GetCurrentMessage(); + ScopedFreeze freeze(this, GetTextObjectModel()); + + TextfieldController* controller = textfield_->GetController(); + bool handled = false; + if (controller) { + KeyEvent event(*msg); + handled = controller->HandleKeyEvent(textfield_, event); + } + + if (!handled) { + OnBeforePossibleChange(); + + if (msg->wParam == ui::VKEY_HOME || msg->wParam == ui::VKEY_END) { + // DefWindowProc() might reset the keyboard layout when it receives a + // keydown event for VKEY_HOME or VKEY_END. When the window was created + // with WS_EX_LAYOUTRTL and the current keyboard layout is not a RTL one, + // if the input text is pure LTR text, the layout changes to the first RTL + // keyboard layout in keyboard layout queue; if the input text is + // bidirectional text, the layout changes to the keyboard layout of the + // first RTL character in input text. When the window was created without + // WS_EX_LAYOUTRTL and the current keyboard layout is not a LTR one, if + // the input text is pure RTL text, the layout changes to English; if the + // input text is bidirectional text, the layout changes to the keyboard + // layout of the first LTR character in input text. Such keyboard layout + // change behavior is surprising and inconsistent with keyboard behavior + // elsewhere, so reset the layout in this case. + HKL layout = GetKeyboardLayout(0); + DefWindowProc(msg->message, msg->wParam, msg->lParam); + ActivateKeyboardLayout(layout, KLF_REORDER); + } else { + DefWindowProc(msg->message, msg->wParam, msg->lParam); + } + + // CRichEditCtrl automatically turns on IMF_AUTOKEYBOARD when the user + // inputs an RTL character, making it difficult for the user to control + // what language is set as they type. Force this off to make the edit's + // behavior more stable. + const int lang_options = SendMessage(EM_GETLANGOPTIONS, 0, 0); + if (lang_options & IMF_AUTOKEYBOARD) + SendMessage(EM_SETLANGOPTIONS, 0, lang_options & ~IMF_AUTOKEYBOARD); + + OnAfterPossibleChange(true); + } +} + +void NativeTextfieldWin::OnBeforePossibleChange() { + // Record our state. + text_before_change_ = GetText(); +} + +void NativeTextfieldWin::OnAfterPossibleChange(bool should_redraw_text) { + // Prevent the user from selecting the "phantom newline" at the end of the + // edit. If they try, we just silently move the end of the selection back to + // the end of the real text. + CHARRANGE new_sel; + GetSel(new_sel); + const int length = GetTextLength(); + if (new_sel.cpMax > length) { + new_sel.cpMax = length; + if (new_sel.cpMin > length) + new_sel.cpMin = length; + SetSel(new_sel); + } + + string16 new_text(GetText()); + if (new_text != text_before_change_) { + if (ime_discard_composition_ && ime_composition_start_ >= 0 && + ime_composition_length_ > 0) { + // A string retrieved with a GetText() call contains a string being + // composed by an IME. We remove the composition string from this search + // string. + new_text.erase(ime_composition_start_, ime_composition_length_); + ime_composition_start_ = 0; + ime_composition_length_ = 0; + if (new_text.empty()) + return; + } + textfield_->SyncText(); + UpdateAccessibleValue(textfield_->text()); + + if (should_redraw_text) { + CHARRANGE original_sel; + GetSel(original_sel); + string16 text(GetText()); + ScopedSuspendUndo suspend_undo(GetTextObjectModel()); + + SelectAll(); + ReplaceSel(reinterpret_cast<LPCTSTR>(text.c_str()), true); + SetSel(original_sel); + } + } +} + +LONG NativeTextfieldWin::ClipXCoordToVisibleText(LONG x, + bool is_triple_click) const { + // Clip the X coordinate to the left edge of the text. Careful: + // PosFromChar(0) may return a negative X coordinate if the beginning of the + // text has scrolled off the edit, so don't go past the clip rect's edge. + PARAFORMAT2 pf2; + GetParaFormat(pf2); + // Calculation of the clipped coordinate is more complicated if the paragraph + // layout is RTL layout, or if there is RTL characters inside the LTR layout + // paragraph. + bool ltr_text_in_ltr_layout = true; + if ((pf2.wEffects & PFE_RTLPARA) || + base::i18n::StringContainsStrongRTLChars(GetText())) { + ltr_text_in_ltr_layout = false; + } + const int length = GetTextLength(); + RECT r; + GetRect(&r); + // The values returned by PosFromChar() seem to refer always + // to the left edge of the character's bounding box. + const LONG first_position_x = PosFromChar(0).x; + LONG min_x = first_position_x; + if (!ltr_text_in_ltr_layout) { + for (int i = 1; i < length; ++i) + min_x = std::min(min_x, PosFromChar(i).x); + } + const LONG left_bound = std::max(r.left, min_x); + + // PosFromChar(length) is a phantom character past the end of the text. It is + // not necessarily a right bound; in RTL controls it may be a left bound. So + // treat it as a right bound only if it is to the right of the first + // character. + LONG right_bound = r.right; + LONG end_position_x = PosFromChar(length).x; + if (end_position_x >= first_position_x) { + right_bound = std::min(right_bound, end_position_x); // LTR case. + } + // For trailing characters that are 2 pixels wide of less (like "l" in some + // fonts), we have a problem: + // * Clicks on any pixel within the character will place the cursor before + // the character. + // * Clicks on the pixel just after the character will not allow triple- + // click to work properly (true for any last character width). + // So, we move to the last pixel of the character when this is a + // triple-click, and moving to one past the last pixel in all other + // scenarios. This way, all clicks that can move the cursor will place it at + // the end of the text, but triple-click will still work. + if (x < left_bound) { + return (is_triple_click && ltr_text_in_ltr_layout) ? left_bound - 1 : + left_bound; + } + if ((length == 0) || (x < right_bound)) + return x; + return is_triple_click ? (right_bound - 1) : right_bound; +} + +void NativeTextfieldWin::SetContainsMouse(bool contains_mouse) { + if (contains_mouse == contains_mouse_) + return; + + contains_mouse_ = contains_mouse; + + if (!textfield_->draw_border()) + return; + + if (contains_mouse_) { + // Register for notification when the mouse leaves. Need to do this so + // that we can reset contains mouse properly. + TRACKMOUSEEVENT tme; + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = m_hWnd; + tme.dwHoverTime = 0; + TrackMouseEvent(&tme); + } + RedrawWindow(NULL, NULL, RDW_INVALIDATE | RDW_FRAME); +} + +ITextDocument* NativeTextfieldWin::GetTextObjectModel() const { + if (!text_object_model_) { + base::win::ScopedComPtr<IRichEditOle, &IID_IRichEditOle> ole_interface; + ole_interface.Attach(GetOleInterface()); + if (ole_interface) + text_object_model_.QueryFrom(ole_interface); + } + return text_object_model_; +} + +void NativeTextfieldWin::BuildContextMenu() { + if (context_menu_contents_.get()) + return; + context_menu_contents_.reset(new ui::SimpleMenuModel(this)); + context_menu_contents_->AddItemWithStringId(IDS_APP_UNDO, IDS_APP_UNDO); + context_menu_contents_->AddSeparator(); + context_menu_contents_->AddItemWithStringId(IDS_APP_CUT, IDS_APP_CUT); + context_menu_contents_->AddItemWithStringId(IDS_APP_COPY, IDS_APP_COPY); + context_menu_contents_->AddItemWithStringId(IDS_APP_PASTE, IDS_APP_PASTE); + context_menu_contents_->AddSeparator(); + context_menu_contents_->AddItemWithStringId(IDS_APP_SELECT_ALL, + IDS_APP_SELECT_ALL); + context_menu_.reset(new Menu2(context_menu_contents_.get())); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldWrapper, public: + +// static +NativeTextfieldWrapper* NativeTextfieldWrapper::CreateWrapper( + Textfield* field) { + if (views::Widget::IsPureViews()) + return new NativeTextfieldViews(field); + return new NativeTextfieldWin(field); +} + +} // namespace views diff --git a/ui/views/controls/textfield/native_textfield_win.h b/ui/views/controls/textfield/native_textfield_win.h new file mode 100644 index 0000000..307060c --- /dev/null +++ b/ui/views/controls/textfield/native_textfield_win.h @@ -0,0 +1,294 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_WIN_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_WIN_H_ +#pragma once + +#include <atlbase.h> +#include <atlapp.h> +#include <atlcrack.h> +#include <atlctrls.h> +#include <atlmisc.h> +#include <oleacc.h> +#include <tom.h> // For ITextDocument, a COM interface to CRichEditCtrl +#include <vsstyle.h> + +#include "base/string16.h" +#include "base/win/scoped_comptr.h" +#include "ui/base/models/simple_menu_model.h" +#include "ui/gfx/insets.h" +#include "ui/views/controls/textfield/native_textfield_wrapper.h" + +namespace gfx { +class SelectionModel; +} + +namespace views { + +class Menu2; +class NativeViewHost; +class Textfield; + +static const int kDefaultEditStyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | + WS_CLIPSIBLINGS; + +// TODO(beng): make a subclass of NativeControlWin instead. +class NativeTextfieldWin + : public CWindowImpl<NativeTextfieldWin, CRichEditCtrl, + CWinTraits<kDefaultEditStyle> >, + public CRichEditCommands<NativeTextfieldWin>, + public NativeTextfieldWrapper, + public ui::SimpleMenuModel::Delegate { + public: + DECLARE_WND_CLASS(L"ViewsTextfieldEdit"); + + explicit NativeTextfieldWin(Textfield* parent); + ~NativeTextfieldWin(); + + // Returns true if the current point is close enough to the origin point in + // space and time that it would be considered a double click. + VIEWS_EXPORT static bool IsDoubleClick(const POINT& origin, + const POINT& current, + DWORD elapsed_time); + + // Returns true if the virtual key code is a digit coming from the numeric + // keypad (with or without NumLock on). |extended_key| should be set to the + // extended key flag specified in the WM_KEYDOWN/UP where the |key_code| + // originated. + VIEWS_EXPORT static bool IsNumPadDigit(int key_code, bool extended_key); + + // See the code in textfield.cc that calls this for why this is here. + void AttachHack(); + + // Overridden from NativeTextfieldWrapper: + virtual string16 GetText() const OVERRIDE; + virtual void UpdateText() OVERRIDE; + virtual void AppendText(const string16& text) OVERRIDE; + virtual string16 GetSelectedText() const OVERRIDE; + virtual void SelectAll() OVERRIDE; + virtual void ClearSelection() OVERRIDE; + virtual void UpdateBorder() OVERRIDE; + virtual void UpdateTextColor() OVERRIDE; + virtual void UpdateBackgroundColor() OVERRIDE; + virtual void UpdateReadOnly() OVERRIDE; + virtual void UpdateFont() OVERRIDE; + virtual void UpdateIsPassword() OVERRIDE; + virtual void UpdateEnabled() OVERRIDE; + virtual gfx::Insets CalculateInsets() OVERRIDE; + virtual void UpdateHorizontalMargins() OVERRIDE; + virtual void UpdateVerticalMargins() OVERRIDE; + virtual bool SetFocus() OVERRIDE; + virtual View* GetView() OVERRIDE; + virtual gfx::NativeView GetTestingHandle() const OVERRIDE; + virtual bool IsIMEComposing() const OVERRIDE; + virtual void GetSelectedRange(ui::Range* range) const OVERRIDE; + virtual void SelectRange(const ui::Range& range) OVERRIDE; + virtual void GetSelectionModel(gfx::SelectionModel* sel) const OVERRIDE; + virtual void SelectSelectionModel(const gfx::SelectionModel& sel) OVERRIDE; + virtual size_t GetCursorPosition() const OVERRIDE; + virtual bool HandleKeyPressed(const views::KeyEvent& event) OVERRIDE; + virtual bool HandleKeyReleased(const views::KeyEvent& event) OVERRIDE; + virtual void HandleFocus() OVERRIDE; + virtual void HandleBlur() OVERRIDE; + virtual ui::TextInputClient* GetTextInputClient() OVERRIDE; + virtual void ApplyStyleRange(const gfx::StyleRange& style) OVERRIDE; + virtual void ApplyDefaultStyle() OVERRIDE; + virtual void ClearEditHistory() OVERRIDE; + + // Overridden from ui::SimpleMenuModel::Delegate: + virtual bool IsCommandIdChecked(int command_id) const OVERRIDE; + virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE; + virtual bool GetAcceleratorForCommandId( + int command_id, + ui::Accelerator* accelerator) OVERRIDE; + virtual void ExecuteCommand(int command_id) OVERRIDE; + + // Update accessibility information. + void InitializeAccessibilityInfo(); + void UpdateAccessibleState(uint32 state_flag, bool set_value); + void UpdateAccessibleValue(const string16& value); + + // CWindowImpl + BEGIN_MSG_MAP(Edit) + MSG_WM_CHAR(OnChar) + MSG_WM_CONTEXTMENU(OnContextMenu) + MSG_WM_COPY(OnCopy) + MSG_WM_CUT(OnCut) + MESSAGE_HANDLER_EX(WM_IME_CHAR, OnImeChar) + MESSAGE_HANDLER_EX(WM_IME_STARTCOMPOSITION, OnImeStartComposition) + MESSAGE_HANDLER_EX(WM_IME_COMPOSITION, OnImeComposition) + MESSAGE_HANDLER_EX(WM_IME_ENDCOMPOSITION, OnImeEndComposition) + MSG_WM_KEYDOWN(OnKeyDown) + MSG_WM_LBUTTONDBLCLK(OnLButtonDblClk) + MSG_WM_LBUTTONDOWN(OnLButtonDown) + MSG_WM_LBUTTONUP(OnLButtonUp) + MSG_WM_MBUTTONDOWN(OnNonLButtonDown) + MSG_WM_MOUSEMOVE(OnMouseMove) + MSG_WM_MOUSELEAVE(OnMouseLeave) + MESSAGE_HANDLER_EX(WM_MOUSEWHEEL, OnMouseWheel) + MSG_WM_NCCALCSIZE(OnNCCalcSize) + MSG_WM_NCPAINT(OnNCPaint) + MSG_WM_RBUTTONDOWN(OnNonLButtonDown) + MSG_WM_PASTE(OnPaste) + MSG_WM_SETFOCUS(OnSetFocus) + MSG_WM_SYSCHAR(OnSysChar) // WM_SYSxxx == WM_xxx with ALT down + MSG_WM_SYSKEYDOWN(OnKeyDown) + END_MSG_MAP() + + private: + // This object freezes repainting of the edit until the object is destroyed. + // Some methods of the CRichEditCtrl draw synchronously to the screen. If we + // don't freeze, the user will see a rapid series of calls to these as + // flickers. + // + // Freezing the control while it is already frozen is permitted; the control + // will unfreeze once both freezes are released (the freezes stack). + class ScopedFreeze { + public: + ScopedFreeze(NativeTextfieldWin* edit, ITextDocument* text_object_model); + ~ScopedFreeze(); + + private: + NativeTextfieldWin* const edit_; + ITextDocument* const text_object_model_; + + DISALLOW_COPY_AND_ASSIGN(ScopedFreeze); + }; + + // This object suspends placing any operations on the edit's undo stack until + // the object is destroyed. If we don't do this, some of the operations we + // perform behind the user's back will be undoable by the user, which feels + // bizarre and confusing. + class ScopedSuspendUndo { + public: + explicit ScopedSuspendUndo(ITextDocument* text_object_model); + ~ScopedSuspendUndo(); + + private: + ITextDocument* const text_object_model_; + + DISALLOW_COPY_AND_ASSIGN(ScopedSuspendUndo); + }; + + // message handlers + void OnChar(TCHAR key, UINT repeat_count, UINT flags); + void OnContextMenu(HWND window, const POINT& point); + void OnCopy(); + void OnCut(); + LRESULT OnImeChar(UINT message, WPARAM wparam, LPARAM lparam); + LRESULT OnImeStartComposition(UINT message, WPARAM wparam, LPARAM lparam); + LRESULT OnImeComposition(UINT message, WPARAM wparam, LPARAM lparam); + LRESULT OnImeEndComposition(UINT message, WPARAM wparam, LPARAM lparam); + void OnKeyDown(TCHAR key, UINT repeat_count, UINT flags); + void OnLButtonDblClk(UINT keys, const CPoint& point); + void OnLButtonDown(UINT keys, const CPoint& point); + void OnLButtonUp(UINT keys, const CPoint& point); + void OnMouseLeave(); + LRESULT OnMouseWheel(UINT message, WPARAM w_param, LPARAM l_param); + void OnMouseMove(UINT keys, const CPoint& point); + int OnNCCalcSize(BOOL w_param, LPARAM l_param); + void OnNCPaint(HRGN region); + void OnNonLButtonDown(UINT keys, const CPoint& point); + void OnPaste(); + void OnSetFocus(HWND hwnd); + void OnSysChar(TCHAR ch, UINT repeat_count, UINT flags); + + // Helper function for OnChar() and OnKeyDown() that handles keystrokes that + // could change the text in the edit. + // Note: This function assumes GetCurrentMessage() returns a MSG with + // msg > WM_KEYFIRST and < WM_KEYLAST. + void HandleKeystroke(); + + // Every piece of code that can change the edit should call these functions + // before and after the change. These functions determine if anything + // meaningful changed, and do any necessary updating and notification. + void OnBeforePossibleChange(); + + // When a user types a BIDI mirroring character (e.g. left parenthesis + // U+0028, which should be rendered as '(' in LTR context unless surrounded + // by RTL characters in both sides, and it should be rendered as ')' in RTL + // context unless surrounded by LTR characters in both sides.), the textfield + // does not properly mirror the character when necessary. However, if we + // explicitly set the text in the edit to the entire current string, then + // the BIDI mirroring characters will be mirrored properly. When + // |should_redraw_text| is true, we explicitly set the text in the edit to + // the entire current string any time the text changes. + void OnAfterPossibleChange(bool should_redraw_text); + + // Given an X coordinate in client coordinates, returns that coordinate + // clipped to be within the horizontal bounds of the visible text. + // + // This is used in our mouse handlers to work around quirky behaviors of the + // underlying CRichEditCtrl like not supporting triple-click when the user + // doesn't click on the text itself. + // + // |is_triple_click| should be true iff this is the third click of a triple + // click. Sadly, we need to clip slightly differently in this case. + LONG ClipXCoordToVisibleText(LONG x, bool is_triple_click) const; + + // Sets whether the mouse is in the edit. As necessary this redraws the + // edit. + void SetContainsMouse(bool contains_mouse); + + // Getter for the text_object_model_, used by the ScopedFreeze class. Note + // that the pointer returned here is only valid as long as the Edit is still + // alive. + ITextDocument* GetTextObjectModel() const; + + // Generates the contents of the context menu. + void BuildContextMenu(); + + // The Textfield this object is bound to. + Textfield* textfield_; + + // We need to know if the user triple-clicks, so track double click points + // and times so we can see if subsequent clicks are actually triple clicks. + bool tracking_double_click_; + CPoint double_click_point_; + DWORD double_click_time_; + + // Used to discard unnecessary WM_MOUSEMOVE events after the first such + // unnecessary event. See detailed comments in OnMouseMove(). + bool can_discard_mousemove_; + + // The text of this control before a possible change. + string16 text_before_change_; + + // If true, the mouse is over the edit. + bool contains_mouse_; + + static bool did_load_library_; + + // The contents of the context menu for the edit. + scoped_ptr<ui::SimpleMenuModel> context_menu_contents_; + scoped_ptr<Menu2> context_menu_; + + // Border insets. + gfx::Insets content_insets_; + + // This interface is useful for accessing the CRichEditCtrl at a low level. + mutable base::win::ScopedComPtr<ITextDocument> text_object_model_; + + // The position and the length of the ongoing composition string. + // These values are used for removing a composition string from a search + // text to emulate Firefox. + bool ime_discard_composition_; + int ime_composition_start_; + int ime_composition_length_; + + // TODO(beng): remove this when we are a subclass of NativeControlWin. + NativeViewHost* container_view_; + + COLORREF bg_color_; + + // The accessibility state of this object. + int accessibility_state_; + + DISALLOW_COPY_AND_ASSIGN(NativeTextfieldWin); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_WIN_H_ diff --git a/ui/views/controls/textfield/native_textfield_wrapper.h b/ui/views/controls/textfield/native_textfield_wrapper.h new file mode 100644 index 0000000..3bc3f1d --- /dev/null +++ b/ui/views/controls/textfield/native_textfield_wrapper.h @@ -0,0 +1,153 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_WRAPPER_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_WRAPPER_H_ +#pragma once + +#include "base/string16.h" +#include "ui/gfx/native_widget_types.h" +#include "views/views_export.h" + +namespace gfx { +class Insets; +class SelectionModel; +struct StyleRange; +} // namespace gfx + +namespace ui { +class Range; +class TextInputClient; +} // namespace ui + +namespace views { + +class KeyEvent; +class Textfield; +class View; + +// An interface implemented by an object that provides a platform-native +// text field. +class VIEWS_EXPORT NativeTextfieldWrapper { + public: + // The Textfield calls this when it is destroyed to clean up the wrapper + // object. + virtual ~NativeTextfieldWrapper() {} + + // Gets the text displayed in the wrapped native text field. + virtual string16 GetText() const = 0; + + // Updates the text displayed with the text held by the Textfield. + virtual void UpdateText() = 0; + + // Adds the specified text to the text already displayed by the wrapped native + // text field. + virtual void AppendText(const string16& text) = 0; + + // Gets the text that is selected in the wrapped native text field. + virtual string16 GetSelectedText() const = 0; + + // Selects all the text in the edit. Use this in place of SetSelAll() to + // avoid selecting the "phantom newline" at the end of the edit. + virtual void SelectAll() = 0; + + // Clears the selection within the edit field and sets the caret to the end. + virtual void ClearSelection() = 0; + + // Updates the border display for the native text field with the state desired + // by the Textfield. + virtual void UpdateBorder() = 0; + + // Updates the text color used when painting the native text field. + virtual void UpdateTextColor() = 0; + + // Updates the background color used when painting the native text field. + virtual void UpdateBackgroundColor() = 0; + + // Updates the read-only state of the native text field. + virtual void UpdateReadOnly() = 0; + + // Updates the font used to render text in the native text field. + virtual void UpdateFont() = 0; + + // Updates the visibility of the text in the native text field. + virtual void UpdateIsPassword() = 0; + + // Updates the enabled state of the native text field. + virtual void UpdateEnabled() = 0; + + // Returns the insets for the text field. + virtual gfx::Insets CalculateInsets() = 0; + + // Updates the horizontal margins for the native text field. + virtual void UpdateHorizontalMargins() = 0; + + // Updates the vertical margins for the native text field. + virtual void UpdateVerticalMargins() = 0; + + // Sets the focus to the text field. Returns false if the wrapper + // didn't take focus. + virtual bool SetFocus() = 0; + + // Retrieves the views::View that hosts the native control. + virtual View* GetView() = 0; + + // Returns a handle to the underlying native view for testing. + virtual gfx::NativeView GetTestingHandle() const = 0; + + // Returns whether or not an IME is composing text. + virtual bool IsIMEComposing() const = 0; + + // Gets the selected range. + virtual void GetSelectedRange(ui::Range* range) const = 0; + + // Selects the text given by |range|. + virtual void SelectRange(const ui::Range& range) = 0; + + // Gets the selection model. + virtual void GetSelectionModel(gfx::SelectionModel* sel) const = 0; + + // Selects the text given by |sel|. + virtual void SelectSelectionModel(const gfx::SelectionModel& sel) = 0; + + // Returns the currnet cursor position. + virtual size_t GetCursorPosition() const = 0; + + // Following methods are to forward key/focus related events to the + // views wrapper so that TextfieldViews can handle key inputs without + // having focus. + + // Invoked when a key is pressed/release on Textfield. Subclasser + // should return true if the event has been processed and false + // otherwise. + // See also View::OnKeyPressed/OnKeyReleased. + virtual bool HandleKeyPressed(const views::KeyEvent& e) = 0; + virtual bool HandleKeyReleased(const views::KeyEvent& e) = 0; + + // Invoked when focus is being moved from or to the Textfield. + // See also View::OnFocus/OnBlur. + virtual void HandleFocus() = 0; + virtual void HandleBlur() = 0; + + // Returns the View's TextInputClient instance or NULL if the View doesn't + // support text input. + virtual ui::TextInputClient* GetTextInputClient() = 0; + + // Applies the |style| to the text specified by its range. + // See |Textfield::ApplyStyleRange| for detail. + virtual void ApplyStyleRange(const gfx::StyleRange& style) = 0; + + // Applies the default style to the textfield. + virtual void ApplyDefaultStyle() = 0; + + // Clears Edit history. + virtual void ClearEditHistory() = 0; + + // Creates an appropriate NativeTextfieldWrapper for the platform. + static NativeTextfieldWrapper* CreateWrapper(Textfield* field); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_WRAPPER_H_ diff --git a/ui/views/controls/textfield/textfield.cc b/ui/views/controls/textfield/textfield.cc new file mode 100644 index 0000000..74395f2 --- /dev/null +++ b/ui/views/controls/textfield/textfield.cc @@ -0,0 +1,453 @@ +// 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 "ui/views/controls/textfield/textfield.h" + +#if defined(TOOLKIT_USES_GTK) +#include <gdk/gdkkeysyms.h> +#endif + +#include <string> + +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "ui/base/accessibility/accessible_view_state.h" +#include "ui/base/ime/text_input_type.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/range/range.h" +#include "ui/gfx/insets.h" +#include "ui/gfx/selection_model.h" +#include "ui/views/controls/native/native_view_host.h" +#include "ui/views/controls/textfield/native_textfield_wrapper.h" +#include "ui/views/controls/textfield/textfield_controller.h" +#include "ui/views/widget/widget.h" + +#if defined(OS_LINUX) +#include "ui/base/keycodes/keyboard_code_conversion_gtk.h" +#elif defined(OS_WIN) +#include "base/win/win_util.h" +// TODO(beng): this should be removed when the OS_WIN hack from +// ViewHierarchyChanged is removed. +#include "ui/views/controls/textfield/native_textfield_views.h" +#include "ui/views/controls/textfield/native_textfield_win.h" +#endif + +namespace views { + +// static +const char Textfield::kViewClassName[] = "views/Textfield"; + +///////////////////////////////////////////////////////////////////////////// +// Textfield + +Textfield::Textfield() + : native_wrapper_(NULL), + controller_(NULL), + style_(STYLE_DEFAULT), + read_only_(false), + default_width_in_chars_(0), + draw_border_(true), + text_color_(SK_ColorBLACK), + use_default_text_color_(true), + background_color_(SK_ColorWHITE), + use_default_background_color_(true), + initialized_(false), + horizontal_margins_were_set_(false), + vertical_margins_were_set_(false), + text_input_type_(ui::TEXT_INPUT_TYPE_TEXT) { + set_focusable(true); +} + +Textfield::Textfield(StyleFlags style) + : native_wrapper_(NULL), + controller_(NULL), + style_(style), + read_only_(false), + default_width_in_chars_(0), + draw_border_(true), + text_color_(SK_ColorBLACK), + use_default_text_color_(true), + background_color_(SK_ColorWHITE), + use_default_background_color_(true), + initialized_(false), + horizontal_margins_were_set_(false), + vertical_margins_were_set_(false), + text_input_type_(ui::TEXT_INPUT_TYPE_TEXT) { + set_focusable(true); + if (IsPassword()) + SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD); +} + +Textfield::~Textfield() { +} + +void Textfield::SetController(TextfieldController* controller) { + controller_ = controller; +} + +TextfieldController* Textfield::GetController() const { + return controller_; +} + +void Textfield::SetReadOnly(bool read_only) { + read_only_ = read_only; + if (native_wrapper_) { + native_wrapper_->UpdateReadOnly(); + native_wrapper_->UpdateTextColor(); + native_wrapper_->UpdateBackgroundColor(); + } +} + +bool Textfield::IsPassword() const { + return style_ & STYLE_PASSWORD; +} + +void Textfield::SetPassword(bool password) { + if (password) { + style_ = static_cast<StyleFlags>(style_ | STYLE_PASSWORD); + SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD); + } else { + style_ = static_cast<StyleFlags>(style_ & ~STYLE_PASSWORD); + SetTextInputType(ui::TEXT_INPUT_TYPE_TEXT); + } + if (native_wrapper_) + native_wrapper_->UpdateIsPassword(); +} + + +ui::TextInputType Textfield::GetTextInputType() const { + if (read_only() || !IsEnabled()) + return ui::TEXT_INPUT_TYPE_NONE; + return text_input_type_; +} + +void Textfield::SetTextInputType(ui::TextInputType type) { + text_input_type_ = type; + bool should_be_password = type == ui::TEXT_INPUT_TYPE_PASSWORD; + if (IsPassword() != should_be_password) + SetPassword(should_be_password); +} + +void Textfield::SetText(const string16& text) { + text_ = text; + if (native_wrapper_) + native_wrapper_->UpdateText(); +} + +void Textfield::AppendText(const string16& text) { + text_ += text; + if (native_wrapper_) + native_wrapper_->AppendText(text); +} + +void Textfield::SelectAll() { + if (native_wrapper_) + native_wrapper_->SelectAll(); +} + +string16 Textfield::GetSelectedText() const { + if (native_wrapper_) + return native_wrapper_->GetSelectedText(); + return string16(); +} + +void Textfield::ClearSelection() const { + if (native_wrapper_) + native_wrapper_->ClearSelection(); +} + +bool Textfield::HasSelection() const { + ui::Range range; + if (native_wrapper_) + native_wrapper_->GetSelectedRange(&range); + return !range.is_empty(); +} + +void Textfield::SetTextColor(SkColor color) { + text_color_ = color; + use_default_text_color_ = false; + if (native_wrapper_) + native_wrapper_->UpdateTextColor(); +} + +void Textfield::UseDefaultTextColor() { + use_default_text_color_ = true; + if (native_wrapper_) + native_wrapper_->UpdateTextColor(); +} + +void Textfield::SetBackgroundColor(SkColor color) { + background_color_ = color; + use_default_background_color_ = false; + if (native_wrapper_) + native_wrapper_->UpdateBackgroundColor(); +} + +void Textfield::UseDefaultBackgroundColor() { + use_default_background_color_ = true; + if (native_wrapper_) + native_wrapper_->UpdateBackgroundColor(); +} + +void Textfield::SetFont(const gfx::Font& font) { + font_ = font; + if (native_wrapper_) + native_wrapper_->UpdateFont(); + PreferredSizeChanged(); +} + +void Textfield::SetHorizontalMargins(int left, int right) { + margins_.Set(margins_.top(), left, margins_.bottom(), right); + horizontal_margins_were_set_ = true; + if (native_wrapper_) + native_wrapper_->UpdateHorizontalMargins(); + PreferredSizeChanged(); +} + +void Textfield::SetVerticalMargins(int top, int bottom) { + margins_.Set(top, margins_.left(), bottom, margins_.right()); + vertical_margins_were_set_ = true; + if (native_wrapper_) + native_wrapper_->UpdateVerticalMargins(); + PreferredSizeChanged(); +} + +void Textfield::RemoveBorder() { + if (!draw_border_) + return; + + draw_border_ = false; + if (native_wrapper_) + native_wrapper_->UpdateBorder(); +} + +bool Textfield::GetHorizontalMargins(int* left, int* right) { + if (!horizontal_margins_were_set_) + return false; + *left = margins_.left(); + *right = margins_.right(); + return true; +} + +bool Textfield::GetVerticalMargins(int* top, int* bottom) { + if (!vertical_margins_were_set_) + return false; + *top = margins_.top(); + *bottom = margins_.bottom(); + return true; +} + +void Textfield::UpdateAllProperties() { + if (native_wrapper_) { + native_wrapper_->UpdateText(); + native_wrapper_->UpdateTextColor(); + native_wrapper_->UpdateBackgroundColor(); + native_wrapper_->UpdateReadOnly(); + native_wrapper_->UpdateFont(); + native_wrapper_->UpdateEnabled(); + native_wrapper_->UpdateBorder(); + native_wrapper_->UpdateIsPassword(); + native_wrapper_->UpdateHorizontalMargins(); + native_wrapper_->UpdateVerticalMargins(); + } +} + +void Textfield::SyncText() { + if (native_wrapper_) { + string16 new_text = native_wrapper_->GetText(); + if (new_text != text_) { + text_ = new_text; + if (controller_) + controller_->ContentsChanged(this, text_); + } + } +} + +bool Textfield::IsIMEComposing() const { + return native_wrapper_ && native_wrapper_->IsIMEComposing(); +} + +void Textfield::GetSelectedRange(ui::Range* range) const { + DCHECK(native_wrapper_); + native_wrapper_->GetSelectedRange(range); +} + +void Textfield::SelectRange(const ui::Range& range) { + DCHECK(native_wrapper_); + native_wrapper_->SelectRange(range); +} + +void Textfield::GetSelectionModel(gfx::SelectionModel* sel) const { + DCHECK(native_wrapper_); + native_wrapper_->GetSelectionModel(sel); +} + +void Textfield::SelectSelectionModel(const gfx::SelectionModel& sel) { + DCHECK(native_wrapper_); + native_wrapper_->SelectSelectionModel(sel); +} + +size_t Textfield::GetCursorPosition() const { + DCHECK(native_wrapper_); + return native_wrapper_->GetCursorPosition(); +} + +void Textfield::ApplyStyleRange(const gfx::StyleRange& style) { + DCHECK(native_wrapper_); + return native_wrapper_->ApplyStyleRange(style); +} + +void Textfield::ApplyDefaultStyle() { + DCHECK(native_wrapper_); + native_wrapper_->ApplyDefaultStyle(); +} + +void Textfield::ClearEditHistory() { + DCHECK(native_wrapper_); + native_wrapper_->ClearEditHistory(); +} + +void Textfield::SetAccessibleName(const string16& name) { + accessible_name_ = name; +} + +//////////////////////////////////////////////////////////////////////////////// +// Textfield, View overrides: + +void Textfield::Layout() { + if (native_wrapper_) { + native_wrapper_->GetView()->SetBoundsRect(GetLocalBounds()); + native_wrapper_->GetView()->Layout(); + } +} + +gfx::Size Textfield::GetPreferredSize() { + gfx::Insets insets; + if (draw_border_ && native_wrapper_) + insets = native_wrapper_->CalculateInsets(); + return gfx::Size(font_.GetExpectedTextWidth(default_width_in_chars_) + + insets.width(), font_.GetHeight() + insets.height()); +} + +bool Textfield::IsFocusable() const { + return View::IsFocusable() && !read_only_; +} + +void Textfield::AboutToRequestFocusFromTabTraversal(bool reverse) { + SelectAll(); +} + +bool Textfield::SkipDefaultKeyEventProcessing(const KeyEvent& e) { + // TODO(hamaji): Figure out which keyboard combinations we need to add here, + // similar to LocationBarView::SkipDefaultKeyEventProcessing. + ui::KeyboardCode key = e.key_code(); + if (key == ui::VKEY_BACK) + return true; // We'll handle BackSpace ourselves. + +#if defined(USE_AURA) + NOTIMPLEMENTED(); +#elif defined(OS_WIN) + // We don't translate accelerators for ALT + NumPad digit on Windows, they are + // used for entering special characters. We do translate alt-home. + if (e.IsAltDown() && (key != ui::VKEY_HOME) && + NativeTextfieldWin::IsNumPadDigit(key, + (e.flags() & ui::EF_EXTENDED) != 0)) + return true; +#endif + return false; +} + +void Textfield::OnPaintBackground(gfx::Canvas* canvas) { + // Overridden to be public - gtk_views_entry.cc wants to call it. + View::OnPaintBackground(canvas); +} + +void Textfield::OnPaintFocusBorder(gfx::Canvas* canvas) { + if (NativeViewHost::kRenderNativeControlFocus) + View::OnPaintFocusBorder(canvas); +} + +bool Textfield::OnKeyPressed(const views::KeyEvent& e) { + return native_wrapper_ && native_wrapper_->HandleKeyPressed(e); +} + +bool Textfield::OnKeyReleased(const views::KeyEvent& e) { + return native_wrapper_ && native_wrapper_->HandleKeyReleased(e); +} + +void Textfield::OnFocus() { + if (native_wrapper_) + native_wrapper_->HandleFocus(); + + // Forward the focus to the wrapper if it exists. + if (!native_wrapper_ || !native_wrapper_->SetFocus()) { + // If there is no wrapper or the wrapper didn't take focus, call + // View::Focus to clear the native focus so that we still get + // keyboard messages. + View::OnFocus(); + } +} + +void Textfield::OnBlur() { + if (native_wrapper_) + native_wrapper_->HandleBlur(); +} + +void Textfield::GetAccessibleState(ui::AccessibleViewState* state) { + state->role = ui::AccessibilityTypes::ROLE_TEXT; + state->name = accessible_name_; + if (read_only()) + state->state |= ui::AccessibilityTypes::STATE_READONLY; + if (IsPassword()) + state->state |= ui::AccessibilityTypes::STATE_PROTECTED; + state->value = text_; + + DCHECK(native_wrapper_); + ui::Range range; + native_wrapper_->GetSelectedRange(&range); + state->selection_start = range.start(); + state->selection_end = range.end(); +} + +ui::TextInputClient* Textfield::GetTextInputClient() { + return native_wrapper_ ? native_wrapper_->GetTextInputClient() : NULL; +} + +void Textfield::OnEnabledChanged() { + View::OnEnabledChanged(); + if (native_wrapper_) + native_wrapper_->UpdateEnabled(); +} + +void Textfield::ViewHierarchyChanged(bool is_add, View* parent, View* child) { + if (is_add && !native_wrapper_ && GetWidget() && !initialized_) { + initialized_ = true; + + // The native wrapper's lifetime will be managed by the view hierarchy after + // we call AddChildView. + native_wrapper_ = NativeTextfieldWrapper::CreateWrapper(this); + AddChildView(native_wrapper_->GetView()); + // TODO(beng): Move this initialization to NativeTextfieldWin once it + // subclasses NativeControlWin. + UpdateAllProperties(); + +#if defined(OS_WIN) && !defined(USE_AURA) + if (!views::Widget::IsPureViews()) { + // TODO(beng): remove this once NativeTextfieldWin subclasses + // NativeControlWin. This is currently called to perform post-AddChildView + // initialization for the wrapper. The GTK version subclasses things + // correctly and doesn't need this. + // + // Remove the include for native_textfield_win.h above when you fix this. + static_cast<NativeTextfieldWin*>(native_wrapper_)->AttachHack(); + } +#endif + } +} + +std::string Textfield::GetClassName() const { + return kViewClassName; +} + +} // namespace views diff --git a/ui/views/controls/textfield/textfield.h b/ui/views/controls/textfield/textfield.h new file mode 100644 index 0000000..abdd590 --- /dev/null +++ b/ui/views/controls/textfield/textfield.h @@ -0,0 +1,315 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_H_ +#pragma once + +#include "build/build_config.h" + +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/string16.h" +#include "third_party/skia/include/core/SkColor.h" +#include "ui/base/ime/text_input_type.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/gfx/font.h" +#include "ui/gfx/native_widget_types.h" +#include "ui/views/controls/textfield/native_textfield_wrapper.h" +#include "views/view.h" + +#if !defined(OS_LINUX) +#include "base/logging.h" +#endif + +namespace gfx { +struct StyleRange; +} // namespace gfx + +namespace ui { +class Range; +class TextInputClient; +} // namespace ui + +namespace views { + +class KeyEvent; +class TextfieldController; + +// This class implements a View that wraps a native text (edit) field. +class VIEWS_EXPORT Textfield : public View { + public: + // The button's class name. + static const char kViewClassName[]; + + enum StyleFlags { + STYLE_DEFAULT = 0, + STYLE_PASSWORD = 1 << 0, + STYLE_LOWERCASE = 1 << 1 + }; + + Textfield(); + explicit Textfield(StyleFlags style); + virtual ~Textfield(); + + // TextfieldController accessors + void SetController(TextfieldController* controller); + TextfieldController* GetController() const; + + // Gets/Sets whether or not the Textfield is read-only. + bool read_only() const { return read_only_; } + void SetReadOnly(bool read_only); + + // Gets/Sets whether or not this Textfield is a password field. + // TODO(bryeung): Currently this is only used in + // chrome/browser/chromeos/options/wifi_config_view.cc, which is being + // converted to WebUI. Please remove this when that happens. + bool IsPassword() const; + void SetPassword(bool password); + + // Gets/Sets the input type of this textfield. + ui::TextInputType GetTextInputType() const; + void SetTextInputType(ui::TextInputType type); + + // Gets/Sets the text currently displayed in the Textfield. + const string16& text() const { return text_; } + + // Sets the text currently displayed in the Textfield. This doesn't + // change the cursor position if the current cursor is within the + // new text's range, or moves the cursor to the end if the cursor is + // out of the new text's range. + void SetText(const string16& text); + + // Appends the given string to the previously-existing text in the field. + void AppendText(const string16& text); + + // Returns the text that is currently selected. + string16 GetSelectedText() const; + + // Causes the edit field to be fully selected. + void SelectAll(); + + // Clears the selection within the edit field and sets the caret to the end. + void ClearSelection() const; + + // Checks if there is any selected text. + bool HasSelection() const; + + // Accessor for |style_|. + StyleFlags style() const { return style_; } + + // Gets/Sets the text color to be used when painting the Textfield. + // Call |UseDefaultTextColor| to return to the system default colors. + SkColor text_color() const { return text_color_; } + void SetTextColor(SkColor color); + + // Gets/Sets whether the default text color should be used when painting the + // Textfield. + bool use_default_text_color() const { + return use_default_text_color_; + } + void UseDefaultTextColor(); + + // Gets/Sets the background color to be used when painting the Textfield. + // Call |UseDefaultBackgroundColor| to return to the system default colors. + SkColor background_color() const { return background_color_; } + void SetBackgroundColor(SkColor color); + + // Gets/Sets whether the default background color should be used when painting + // the Textfield. + bool use_default_background_color() const { + return use_default_background_color_; + } + void UseDefaultBackgroundColor(); + + // Gets/Sets the font used when rendering the text within the Textfield. + const gfx::Font& font() const { return font_; } + void SetFont(const gfx::Font& font); + + // Sets the left and right margin (in pixels) within the text box. On Windows + // this is accomplished by packing the left and right margin into a single + // 32 bit number, so the left and right margins are effectively 16 bits. + void SetHorizontalMargins(int left, int right); + + // Sets the top and bottom margins (in pixels) within the textfield. + // NOTE: in most cases height could be changed instead. + void SetVerticalMargins(int top, int bottom); + + // Sets the default width of the text control. See default_width_in_chars_. + void set_default_width_in_chars(int default_width) { + default_width_in_chars_ = default_width; + } + + // Removes the border from the edit box, giving it a 2D look. + bool draw_border() const { return draw_border_; } + void RemoveBorder(); + + // Sets the text to display when empty. + void set_text_to_display_when_empty(const string16& text) { + text_to_display_when_empty_ = text; +#if !defined(OS_LINUX) + NOTIMPLEMENTED(); +#endif + } + const string16& text_to_display_when_empty() { + return text_to_display_when_empty_; + } + + // Getter for the horizontal margins that were set. Returns false if + // horizontal margins weren't set. + bool GetHorizontalMargins(int* left, int* right); + + // Getter for the vertical margins that were set. Returns false if vertical + // margins weren't set. + bool GetVerticalMargins(int* top, int* bottom); + + // Updates all properties on the textfield. This is invoked internally. + // Users of Textfield never need to invoke this directly. + void UpdateAllProperties(); + + // Invoked by the edit control when the value changes. This method set + // the text_ member variable to the value contained in edit control. + // This is important because the edit control can be replaced if it has + // been deleted during a window close. + void SyncText(); + + // Returns whether or not an IME is composing text. + bool IsIMEComposing() const; + + // Gets the selected range. This is views-implementation only and + // has to be called after the wrapper is created. + void GetSelectedRange(ui::Range* range) const; + + // Selects the text given by |range|. This is views-implementation only and + // has to be called after the wrapper is created. + void SelectRange(const ui::Range& range); + + // Gets the selection model. This is views-implementation only and + // has to be called after the wrapper is created. + void GetSelectionModel(gfx::SelectionModel* sel) const; + + // Selects the text given by |sel|. This is views-implementation only and + // has to be called after the wrapper is created. + void SelectSelectionModel(const gfx::SelectionModel& sel); + + // Returns the current cursor position. This is views-implementation + // only and has to be called after the wrapper is created. + size_t GetCursorPosition() const; + + // Applies |style| to the text specified by its range. The style will be + // ignored if range is empty or invalid. This is views-implementation only and + // has to be called after the wrapper is created. + void ApplyStyleRange(const gfx::StyleRange& style); + + // Applies the default style to the textfield. This is views-implementation + // only and has to be called after the wrapper is created. + void ApplyDefaultStyle(); + + // Clears Edit history. + void ClearEditHistory(); + + // Set the accessible name of the text field. + void SetAccessibleName(const string16& name); + + // Provided only for testing: + gfx::NativeView GetTestingHandle() const { + return native_wrapper_ ? native_wrapper_->GetTestingHandle() : NULL; + } + NativeTextfieldWrapper* GetNativeWrapperForTesting() const { + return native_wrapper_; + } + + // Overridden from View: + virtual void Layout() OVERRIDE; + virtual gfx::Size GetPreferredSize() OVERRIDE; + virtual bool IsFocusable() const OVERRIDE; + virtual void AboutToRequestFocusFromTabTraversal(bool reverse) OVERRIDE; + virtual bool SkipDefaultKeyEventProcessing(const KeyEvent& e) OVERRIDE; + virtual void OnEnabledChanged() OVERRIDE; + virtual void OnPaintBackground(gfx::Canvas* canvas) OVERRIDE; + virtual void OnPaintFocusBorder(gfx::Canvas* canvas) OVERRIDE; + virtual bool OnKeyPressed(const views::KeyEvent& e) OVERRIDE; + virtual bool OnKeyReleased(const views::KeyEvent& e) OVERRIDE; + virtual void OnFocus() OVERRIDE; + virtual void OnBlur() OVERRIDE; + virtual void GetAccessibleState(ui::AccessibleViewState* state) OVERRIDE; + virtual ui::TextInputClient* GetTextInputClient() OVERRIDE; + + protected: + virtual void ViewHierarchyChanged(bool is_add, View* parent, + View* child) OVERRIDE; + virtual std::string GetClassName() const OVERRIDE; + + // The object that actually implements the native text field. + NativeTextfieldWrapper* native_wrapper_; + + private: + // This is the current listener for events from this Textfield. + TextfieldController* controller_; + + // The mask of style options for this Textfield. + StyleFlags style_; + + // The font used to render the text in the Textfield. + gfx::Font font_; + + // The text displayed in the Textfield. + string16 text_; + + // True if this Textfield cannot accept input and is read-only. + bool read_only_; + + // The default number of average characters for the width of this text field. + // This will be reported as the "desired size". Defaults to 0. + int default_width_in_chars_; + + // Whether the border is drawn. + bool draw_border_; + + // The text color to be used when painting the Textfield, provided + // |use_default_text_color_| is set to false. + SkColor text_color_; + + // When true, the system text color for Textfields is used when painting this + // Textfield. When false, the value of |text_color_| determines the + // Textfield's text color. + bool use_default_text_color_; + + // The background color to be used when painting the Textfield, provided + // |use_default_background_color_| is set to false. + SkColor background_color_; + + // When true, the system background color for Textfields is used when painting + // this Textfield. When false, the value of |background_color_| determines the + // Textfield's background color. + bool use_default_background_color_; + + // TODO(beng): remove this once NativeTextfieldWin subclasses + // NativeControlWin. + bool initialized_; + + // Holds inner textfield margins. + gfx::Insets margins_; + + // Holds whether margins were set. + bool horizontal_margins_were_set_; + bool vertical_margins_were_set_; + + // Text to display when empty. + string16 text_to_display_when_empty_; + + // The accessible name of the text field. + string16 accessible_name_; + + // The input type of this text field. + ui::TextInputType text_input_type_; + + DISALLOW_COPY_AND_ASSIGN(Textfield); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_H_ diff --git a/ui/views/controls/textfield/textfield_controller.h b/ui/views/controls/textfield/textfield_controller.h new file mode 100644 index 0000000..ef2bdda --- /dev/null +++ b/ui/views/controls/textfield/textfield_controller.h @@ -0,0 +1,46 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_CONTROLLER_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_CONTROLLER_H_ +#pragma once + +#include "base/string16.h" + +namespace views { + +class KeyEvent; +class Textfield; + +// This defines the callback interface for other code to be notified of changes +// in the state of a text field. +class TextfieldController { + public: + // This method is called whenever the text in the field is changed by the + // user. It won't be called if the text is changed by calling + // Textfield::SetText() or Textfield::AppendText(). + virtual void ContentsChanged(Textfield* sender, + const string16& new_contents) = 0; + + // This method is called to get notified about keystrokes in the edit. + // Returns true if the message was handled and should not be processed + // further. If it returns false the processing continues. + virtual bool HandleKeyEvent(Textfield* sender, + const KeyEvent& key_event) = 0; + + // Called before performing a user action that may change the textfield. + // It's currently only supported by Views implementation. + virtual void OnBeforeUserAction(Textfield* sender) {} + + // Called after performing a user action that may change the textfield. + // It's currently only supported by Views implementation. + virtual void OnAfterUserAction(Textfield* sender) {} + + protected: + virtual ~TextfieldController() {} +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_CONTROLLER_H_ diff --git a/ui/views/controls/textfield/textfield_views_model.cc b/ui/views/controls/textfield/textfield_views_model.cc new file mode 100644 index 0000000..0895f91 --- /dev/null +++ b/ui/views/controls/textfield/textfield_views_model.cc @@ -0,0 +1,795 @@ +// 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 "ui/views/controls/textfield/textfield_views_model.h" + +#include <algorithm> + +#include "base/i18n/break_iterator.h" +#include "base/logging.h" +#include "base/stl_util.h" +#include "base/utf_string_conversions.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/clipboard/scoped_clipboard_writer.h" +#include "ui/base/range/range.h" +#include "ui/gfx/canvas.h" +#include "ui/gfx/font.h" +#include "ui/gfx/render_text.h" +#include "ui/views/controls/textfield/textfield.h" +#include "views/views_delegate.h" + +namespace views { + +namespace internal { + +// An edit object holds enough information/state to undo/redo the +// change. Two edits are merged when possible, for example, when +// you type new characters in sequence. |Commit()| can be used to +// mark an edit as an independent edit and it shouldn't be merged. +// (For example, when you did undo/redo, or a text is appended via +// API) +class Edit { + public: + enum Type { + INSERT_EDIT, + DELETE_EDIT, + REPLACE_EDIT + }; + + virtual ~Edit() { + } + + // Revert the change made by this edit in |model|. + void Undo(TextfieldViewsModel* model) { + model->ModifyText(new_text_start_, new_text_end(), + old_text_, old_text_start_, + old_cursor_pos_); + } + + // Apply the change of this edit to the |model|. + void Redo(TextfieldViewsModel* model) { + model->ModifyText(old_text_start_, old_text_end(), + new_text_, new_text_start_, + new_cursor_pos_); + } + + // Try to merge the |edit| into this edit. Returns true if merge was + // successful, or false otherwise. Merged edit will be deleted after + // redo and should not be reused. + bool Merge(const Edit* edit) { + if (edit->merge_with_previous()) { + MergeSet(edit); + return true; + } + return mergeable() && edit->mergeable() && DoMerge(edit); + } + + // Commits the edit and marks as un-mergeable. + void Commit() { merge_type_ = DO_NOT_MERGE; } + + private: + friend class InsertEdit; + friend class ReplaceEdit; + friend class DeleteEdit; + + Edit(Type type, + MergeType merge_type, + size_t old_cursor_pos, + string16 old_text, + size_t old_text_start, + bool delete_backward, + size_t new_cursor_pos, + string16 new_text, + size_t new_text_start) + : type_(type), + merge_type_(merge_type), + old_cursor_pos_(old_cursor_pos), + old_text_(old_text), + old_text_start_(old_text_start), + delete_backward_(delete_backward), + new_cursor_pos_(new_cursor_pos), + new_text_(new_text), + new_text_start_(new_text_start) { + } + + // A template method pattern that provides specific merge + // implementation for each type of edit. + virtual bool DoMerge(const Edit* edit) = 0; + + Type type() const { return type_; } + + // Can this edit be merged? + bool mergeable() const { return merge_type_ == MERGEABLE; } + + // Should this edit be forcibly merged with the previous edit? + bool merge_with_previous() const { + return merge_type_ == MERGE_WITH_PREVIOUS; + } + + // Returns the end index of the |old_text_|. + size_t old_text_end() const { return old_text_start_ + old_text_.length(); } + + // Returns the end index of the |new_text_|. + size_t new_text_end() const { return new_text_start_ + new_text_.length(); } + + // Merge the Set edit into the current edit. This is a special case to + // handle an omnibox setting autocomplete string after new character is + // typed in. + void MergeSet(const Edit* edit) { + CHECK_EQ(REPLACE_EDIT, edit->type_); + CHECK_EQ(0U, edit->old_text_start_); + CHECK_EQ(0U, edit->new_text_start_); + string16 old_text = edit->old_text_; + old_text.erase(new_text_start_, new_text_.length()); + old_text.insert(old_text_start_, old_text_); + // SetText() replaces entire text. Set |old_text_| to the entire + // replaced text with |this| edit undone. + old_text_ = old_text; + old_text_start_ = edit->old_text_start_; + delete_backward_ = false; + + new_text_ = edit->new_text_; + new_text_start_ = edit->new_text_start_; + merge_type_ = DO_NOT_MERGE; + } + + Type type_; + + // True if the edit can be marged. + MergeType merge_type_; + // Old cursor position. + size_t old_cursor_pos_; + // Deleted text by this edit. + string16 old_text_; + // The index of |old_text_|. + size_t old_text_start_; + // True if the deletion is made backward. + bool delete_backward_; + // New cursor position. + size_t new_cursor_pos_; + // Added text. + string16 new_text_; + // The index of |new_text_| + size_t new_text_start_; + + DISALLOW_COPY_AND_ASSIGN(Edit); +}; + +class InsertEdit : public Edit { + public: + InsertEdit(bool mergeable, const string16& new_text, size_t at) + : Edit(INSERT_EDIT, + mergeable ? MERGEABLE : DO_NOT_MERGE, + at /* old cursor */, + string16(), + at, + false /* N/A */, + at + new_text.length() /* new cursor */, + new_text, + at) { + } + + // Edit implementation. + virtual bool DoMerge(const Edit* edit) OVERRIDE { + if (edit->type() != INSERT_EDIT || new_text_end() != edit->new_text_start_) + return false; + // If continuous edit, merge it. + // TODO(oshima): gtk splits edits between whitespace. Find out what + // we want to here and implement if necessary. + new_text_ += edit->new_text_; + new_cursor_pos_ = edit->new_cursor_pos_; + return true; + } +}; + +class ReplaceEdit : public Edit { + public: + ReplaceEdit(MergeType merge_type, + const string16& old_text, + size_t old_cursor_pos, + size_t old_text_start, + bool backward, + size_t new_cursor_pos, + const string16& new_text, + size_t new_text_start) + : Edit(REPLACE_EDIT, merge_type, + old_cursor_pos, + old_text, + old_text_start, + backward, + new_cursor_pos, + new_text, + new_text_start) { + } + + // Edit implementation. + virtual bool DoMerge(const Edit* edit) OVERRIDE { + if (edit->type() == DELETE_EDIT || + new_text_end() != edit->old_text_start_ || + edit->old_text_start_ != edit->new_text_start_) + return false; + old_text_ += edit->old_text_; + new_text_ += edit->new_text_; + new_cursor_pos_ = edit->new_cursor_pos_; + return true; + } +}; + +class DeleteEdit : public Edit { + public: + DeleteEdit(bool mergeable, + const string16& text, + size_t text_start, + bool backward) + : Edit(DELETE_EDIT, + mergeable ? MERGEABLE : DO_NOT_MERGE, + (backward ? text_start + text.length() : text_start), + text, + text_start, + backward, + text_start, + string16(), + text_start) { + } + + // Edit implementation. + virtual bool DoMerge(const Edit* edit) OVERRIDE { + if (edit->type() != DELETE_EDIT) + return false; + + if (delete_backward_) { + // backspace can be merged only with backspace at the + // same position. + if (!edit->delete_backward_ || old_text_start_ != edit->old_text_end()) + return false; + old_text_start_ = edit->old_text_start_; + old_text_ = edit->old_text_ + old_text_; + new_cursor_pos_ = edit->new_cursor_pos_; + } else { + // delete can be merged only with delete at the same + // position. + if (edit->delete_backward_ || old_text_start_ != edit->old_text_start_) + return false; + old_text_ += edit->old_text_; + } + return true; + } +}; + +} // namespace internal + +using internal::Edit; +using internal::DeleteEdit; +using internal::InsertEdit; +using internal::ReplaceEdit; +using internal::MergeType; +using internal::DO_NOT_MERGE; +using internal::MERGE_WITH_PREVIOUS; +using internal::MERGEABLE; + +///////////////////////////////////////////////////////////////// +// TextfieldViewsModel: public + +TextfieldViewsModel::Delegate::~Delegate() { +} + +TextfieldViewsModel::TextfieldViewsModel(Delegate* delegate) + : delegate_(delegate), + render_text_(gfx::RenderText::CreateRenderText()), + is_password_(false), + current_edit_(edit_history_.end()) { +} + +TextfieldViewsModel::~TextfieldViewsModel() { + ClearEditHistory(); + ClearComposition(); +} + +const string16& TextfieldViewsModel::GetText() const { + return render_text_->text(); +} + +bool TextfieldViewsModel::SetText(const string16& text) { + bool changed = false; + if (HasCompositionText()) { + ConfirmCompositionText(); + changed = true; + } + if (GetText() != text) { + if (changed) // No need to remember composition. + Undo(); + size_t old_cursor = GetCursorPosition(); + size_t new_cursor = old_cursor > text.length() ? text.length() : old_cursor; + SelectAll(); + // If there is a composition text, don't merge with previous edit. + // Otherwise, force merge the edits. + ExecuteAndRecordReplace( + changed ? DO_NOT_MERGE : MERGE_WITH_PREVIOUS, + old_cursor, + new_cursor, + text, + 0U); + render_text_->SetCursorPosition(new_cursor); + } + ClearSelection(); + return changed; +} + +void TextfieldViewsModel::Append(const string16& text) { + if (HasCompositionText()) + ConfirmCompositionText(); + size_t save = GetCursorPosition(); + if (render_text_->GetTextDirection() == base::i18n::LEFT_TO_RIGHT) + MoveCursorRight(gfx::LINE_BREAK, false); + else + MoveCursorLeft(gfx::LINE_BREAK, false); + InsertText(text); + render_text_->SetCursorPosition(save); + ClearSelection(); +} + +bool TextfieldViewsModel::Delete() { + if (HasCompositionText()) { + // No undo/redo for composition text. + CancelCompositionText(); + return true; + } + if (HasSelection()) { + DeleteSelection(); + return true; + } + if (GetText().length() > GetCursorPosition()) { + size_t cursor_position = GetCursorPosition(); + size_t next_grapheme_index = + render_text_->GetIndexOfNextGrapheme(cursor_position); + ExecuteAndRecordDelete(cursor_position, next_grapheme_index, true); + return true; + } + return false; +} + +bool TextfieldViewsModel::Backspace() { + if (HasCompositionText()) { + // No undo/redo for composition text. + CancelCompositionText(); + return true; + } + if (HasSelection()) { + DeleteSelection(); + return true; + } + if (GetCursorPosition() > 0) { + size_t cursor_position = GetCursorPosition(); + ExecuteAndRecordDelete(cursor_position, cursor_position - 1, true); + return true; + } + return false; +} + +size_t TextfieldViewsModel::GetCursorPosition() const { + return render_text_->GetCursorPosition(); +} + +void TextfieldViewsModel::MoveCursorLeft(gfx::BreakType break_type, + bool select) { + if (HasCompositionText()) + ConfirmCompositionText(); + render_text_->MoveCursorLeft(break_type, select); +} + +void TextfieldViewsModel::MoveCursorRight(gfx::BreakType break_type, + bool select) { + if (HasCompositionText()) + ConfirmCompositionText(); + render_text_->MoveCursorRight(break_type, select); +} + +bool TextfieldViewsModel::MoveCursorTo(const gfx::SelectionModel& selection) { + if (HasCompositionText()) { + ConfirmCompositionText(); + // ConfirmCompositionText() updates cursor position. Need to reflect it in + // the SelectionModel parameter of MoveCursorTo(). + if (render_text_->GetSelectionStart() != selection.selection_end()) + return render_text_->SelectRange(ui::Range( + render_text_->GetSelectionStart(), selection.selection_end())); + gfx::SelectionModel sel(selection.selection_end(), + selection.caret_pos(), + selection.caret_placement()); + return render_text_->MoveCursorTo(sel); + } + return render_text_->MoveCursorTo(selection); +} + +bool TextfieldViewsModel::MoveCursorTo(const gfx::Point& point, bool select) { + if (HasCompositionText()) + ConfirmCompositionText(); + return render_text_->MoveCursorTo(point, select); +} + +string16 TextfieldViewsModel::GetSelectedText() const { + return GetText().substr(render_text_->MinOfSelection(), + (render_text_->MaxOfSelection() - render_text_->MinOfSelection())); +} + +void TextfieldViewsModel::GetSelectedRange(ui::Range* range) const { + range->set_start(render_text_->GetSelectionStart()); + range->set_end(render_text_->GetCursorPosition()); +} + +void TextfieldViewsModel::SelectRange(const ui::Range& range) { + if (HasCompositionText()) + ConfirmCompositionText(); + render_text_->SelectRange(range); +} + +void TextfieldViewsModel::GetSelectionModel(gfx::SelectionModel* sel) const { + *sel = render_text_->selection_model(); +} + +void TextfieldViewsModel::SelectSelectionModel(const gfx::SelectionModel& sel) { + if (HasCompositionText()) + ConfirmCompositionText(); + render_text_->MoveCursorTo(sel); +} + +void TextfieldViewsModel::SelectAll() { + if (HasCompositionText()) + ConfirmCompositionText(); + render_text_->SelectAll(); +} + +void TextfieldViewsModel::SelectWord() { + if (HasCompositionText()) + ConfirmCompositionText(); + render_text_->SelectWord(); +} + +void TextfieldViewsModel::ClearSelection() { + if (HasCompositionText()) + ConfirmCompositionText(); + render_text_->ClearSelection(); +} + +bool TextfieldViewsModel::CanUndo() { + return edit_history_.size() && current_edit_ != edit_history_.end(); +} + +bool TextfieldViewsModel::CanRedo() { + if (!edit_history_.size()) + return false; + // There is no redo iff the current edit is the last element + // in the history. + EditHistory::iterator iter = current_edit_; + return iter == edit_history_.end() || // at the top. + ++iter != edit_history_.end(); +} + +bool TextfieldViewsModel::Undo() { + if (!CanUndo()) + return false; + DCHECK(!HasCompositionText()); + if (HasCompositionText()) // safe guard for release build. + CancelCompositionText(); + + string16 old = GetText(); + size_t old_cursor = GetCursorPosition(); + (*current_edit_)->Commit(); + (*current_edit_)->Undo(this); + + if (current_edit_ == edit_history_.begin()) + current_edit_ = edit_history_.end(); + else + current_edit_--; + return old != GetText() || old_cursor != GetCursorPosition(); +} + +bool TextfieldViewsModel::Redo() { + if (!CanRedo()) + return false; + DCHECK(!HasCompositionText()); + if (HasCompositionText()) // safe guard for release build. + CancelCompositionText(); + + if (current_edit_ == edit_history_.end()) + current_edit_ = edit_history_.begin(); + else + current_edit_ ++; + string16 old = GetText(); + size_t old_cursor = GetCursorPosition(); + (*current_edit_)->Redo(this); + return old != GetText() || old_cursor != GetCursorPosition(); +} + +string16 TextfieldViewsModel::GetVisibleText() const { + return GetVisibleText(0U, GetText().length()); +} + +bool TextfieldViewsModel::Cut() { + if (!HasCompositionText() && HasSelection()) { + ui::ScopedClipboardWriter(views::ViewsDelegate::views_delegate + ->GetClipboard()).WriteText(GetSelectedText()); + // A trick to let undo/redo handle cursor correctly. + // Undoing CUT moves the cursor to the end of the change rather + // than beginning, unlike Delete/Backspace. + // TODO(oshima): Change Delete/Backspace to use DeleteSelection, + // update DeleteEdit and remove this trick. + render_text_->SelectRange(ui::Range(render_text_->GetCursorPosition(), + render_text_->GetSelectionStart())); + DeleteSelection(); + return true; + } + return false; +} + +void TextfieldViewsModel::Copy() { + if (!HasCompositionText() && HasSelection()) { + ui::ScopedClipboardWriter(views::ViewsDelegate::views_delegate + ->GetClipboard()).WriteText(GetSelectedText()); + } +} + +bool TextfieldViewsModel::Paste() { + string16 result; + views::ViewsDelegate::views_delegate->GetClipboard() + ->ReadText(ui::Clipboard::BUFFER_STANDARD, &result); + if (!result.empty()) { + InsertTextInternal(result, false); + return true; + } + return false; +} + +bool TextfieldViewsModel::HasSelection() const { + return !render_text_->EmptySelection(); +} + +void TextfieldViewsModel::DeleteSelection() { + DCHECK(!HasCompositionText()); + DCHECK(HasSelection()); + ExecuteAndRecordDelete(render_text_->GetSelectionStart(), + render_text_->GetCursorPosition(), false); +} + +void TextfieldViewsModel::DeleteSelectionAndInsertTextAt( + const string16& text, size_t position) { + if (HasCompositionText()) + CancelCompositionText(); + ExecuteAndRecordReplace(DO_NOT_MERGE, + GetCursorPosition(), + position + text.length(), + text, + position); +} + +string16 TextfieldViewsModel::GetTextFromRange(const ui::Range& range) const { + if (range.IsValid() && range.GetMin() < GetText().length()) + return GetText().substr(range.GetMin(), range.length()); + return string16(); +} + +void TextfieldViewsModel::GetTextRange(ui::Range* range) const { + *range = ui::Range(0, GetText().length()); +} + +void TextfieldViewsModel::SetCompositionText( + const ui::CompositionText& composition) { + if (HasCompositionText()) + CancelCompositionText(); + else if (HasSelection()) + DeleteSelection(); + + if (composition.text.empty()) + return; + + size_t cursor = GetCursorPosition(); + string16 new_text = GetText(); + render_text_->SetText(new_text.insert(cursor, composition.text)); + ui::Range range(cursor, cursor + composition.text.length()); + render_text_->SetCompositionRange(range); + // TODO(msw): Support multiple composition underline ranges. + + if (composition.selection.IsValid()) { + size_t start = + std::min(range.start() + composition.selection.start(), range.end()); + size_t end = + std::min(range.start() + composition.selection.end(), range.end()); + render_text_->SelectRange(ui::Range(start, end)); + } else { + render_text_->SetCursorPosition(range.end()); + } +} + +void TextfieldViewsModel::ConfirmCompositionText() { + DCHECK(HasCompositionText()); + ui::Range range = render_text_->GetCompositionRange(); + string16 text = GetText().substr(range.start(), range.length()); + // TODO(oshima): current behavior on ChromeOS is a bit weird and not + // sure exactly how this should work. Find out and fix if necessary. + AddOrMergeEditHistory(new InsertEdit(false, text, range.start())); + render_text_->SetCursorPosition(range.end()); + ClearComposition(); + if (delegate_) + delegate_->OnCompositionTextConfirmedOrCleared(); +} + +void TextfieldViewsModel::CancelCompositionText() { + DCHECK(HasCompositionText()); + ui::Range range = render_text_->GetCompositionRange(); + ClearComposition(); + string16 new_text = GetText(); + render_text_->SetText(new_text.erase(range.start(), range.length())); + render_text_->SetCursorPosition(range.start()); + if (delegate_) + delegate_->OnCompositionTextConfirmedOrCleared(); +} + +void TextfieldViewsModel::ClearComposition() { + render_text_->SetCompositionRange(ui::Range::InvalidRange()); +} + +void TextfieldViewsModel::GetCompositionTextRange(ui::Range* range) const { + *range = ui::Range(render_text_->GetCompositionRange()); +} + +bool TextfieldViewsModel::HasCompositionText() const { + return !render_text_->GetCompositionRange().is_empty(); +} + +///////////////////////////////////////////////////////////////// +// TextfieldViewsModel: private + +string16 TextfieldViewsModel::GetVisibleText(size_t begin, size_t end) const { + DCHECK(end >= begin); + if (is_password_) + return string16(end - begin, '*'); + return GetText().substr(begin, end - begin); +} + +void TextfieldViewsModel::InsertTextInternal(const string16& text, + bool mergeable) { + if (HasCompositionText()) { + CancelCompositionText(); + ExecuteAndRecordInsert(text, mergeable); + } else if (HasSelection()) { + ExecuteAndRecordReplaceSelection(mergeable ? MERGEABLE : DO_NOT_MERGE, + text); + } else { + ExecuteAndRecordInsert(text, mergeable); + } +} + +void TextfieldViewsModel::ReplaceTextInternal(const string16& text, + bool mergeable) { + if (HasCompositionText()) { + CancelCompositionText(); + } else if (!HasSelection()) { + size_t cursor = GetCursorPosition(); + const gfx::SelectionModel& model = render_text_->selection_model(); + // When there is no selection, the default is to replace the next grapheme + // with |text|. So, need to find the index of next grapheme first. + size_t next = render_text_->GetIndexOfNextGrapheme(cursor); + if (next == model.selection_end()) + render_text_->MoveCursorTo(model); + else + render_text_->SelectRange(ui::Range(next, model.selection_end())); + } + // Edit history is recorded in InsertText. + InsertTextInternal(text, mergeable); +} + +void TextfieldViewsModel::ClearEditHistory() { + STLDeleteContainerPointers(edit_history_.begin(), + edit_history_.end()); + edit_history_.clear(); + current_edit_ = edit_history_.end(); +} + +void TextfieldViewsModel::ClearRedoHistory() { + if (edit_history_.begin() == edit_history_.end()) + return; + if (current_edit_ == edit_history_.end()) { + ClearEditHistory(); + return; + } + EditHistory::iterator delete_start = current_edit_; + delete_start++; + STLDeleteContainerPointers(delete_start, edit_history_.end()); + edit_history_.erase(delete_start, edit_history_.end()); +} + +void TextfieldViewsModel::ExecuteAndRecordDelete(size_t from, + size_t to, + bool mergeable) { + size_t old_text_start = std::min(from, to); + const string16 text = GetText().substr(old_text_start, + std::abs(static_cast<long>(from - to))); + bool backward = from > to; + Edit* edit = new DeleteEdit(mergeable, text, old_text_start, backward); + bool delete_edit = AddOrMergeEditHistory(edit); + edit->Redo(this); + if (delete_edit) + delete edit; +} + +void TextfieldViewsModel::ExecuteAndRecordReplaceSelection( + MergeType merge_type, const string16& new_text) { + size_t new_text_start = render_text_->MinOfSelection(); + size_t new_cursor_pos = new_text_start + new_text.length(); + ExecuteAndRecordReplace(merge_type, + GetCursorPosition(), + new_cursor_pos, + new_text, + new_text_start); +} + +void TextfieldViewsModel::ExecuteAndRecordReplace(MergeType merge_type, + size_t old_cursor_pos, + size_t new_cursor_pos, + const string16& new_text, + size_t new_text_start) { + size_t old_text_start = render_text_->MinOfSelection(); + bool backward = + render_text_->GetSelectionStart() > render_text_->GetCursorPosition(); + Edit* edit = new ReplaceEdit(merge_type, + GetSelectedText(), + old_cursor_pos, + old_text_start, + backward, + new_cursor_pos, + new_text, + new_text_start); + bool delete_edit = AddOrMergeEditHistory(edit); + edit->Redo(this); + if (delete_edit) + delete edit; +} + +void TextfieldViewsModel::ExecuteAndRecordInsert(const string16& text, + bool mergeable) { + Edit* edit = new InsertEdit(mergeable, text, GetCursorPosition()); + bool delete_edit = AddOrMergeEditHistory(edit); + edit->Redo(this); + if (delete_edit) + delete edit; +} + +bool TextfieldViewsModel::AddOrMergeEditHistory(Edit* edit) { + ClearRedoHistory(); + + if (current_edit_ != edit_history_.end() && (*current_edit_)->Merge(edit)) { + // If a current edit exists and has been merged with a new edit, + // don't add to the history, and return true to delete |edit| after + // redo. + return true; + } + edit_history_.push_back(edit); + if (current_edit_ == edit_history_.end()) { + // If there is no redoable edit, this is the 1st edit because + // RedoHistory has been already deleted. + DCHECK_EQ(1u, edit_history_.size()); + current_edit_ = edit_history_.begin(); + } else { + current_edit_++; + } + return false; +} + +void TextfieldViewsModel::ModifyText(size_t delete_from, + size_t delete_to, + const string16& new_text, + size_t new_text_insert_at, + size_t new_cursor_pos) { + DCHECK_LE(delete_from, delete_to); + string16 text = GetText(); + ClearComposition(); + if (delete_from != delete_to) + render_text_->SetText(text.erase(delete_from, delete_to - delete_from)); + if (!new_text.empty()) + render_text_->SetText(text.insert(new_text_insert_at, new_text)); + render_text_->SetCursorPosition(new_cursor_pos); + // TODO(oshima): mac selects the text that is just undone (but gtk doesn't). + // This looks fine feature and we may want to do the same. +} + +} // namespace views diff --git a/ui/views/controls/textfield/textfield_views_model.h b/ui/views/controls/textfield/textfield_views_model.h new file mode 100644 index 0000000..d13a7b6d --- /dev/null +++ b/ui/views/controls/textfield/textfield_views_model.h @@ -0,0 +1,329 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_VIEWS_MODEL_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_VIEWS_MODEL_H_ +#pragma once + +#include <list> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "base/string16.h" +#include "third_party/skia/include/core/SkColor.h" +#include "ui/base/ime/composition_text.h" +#include "ui/gfx/rect.h" +#include "ui/gfx/render_text.h" +#include "views/views_export.h" + +namespace gfx { +class RenderText; +} // namespace gfx + +namespace ui { +class Range; +} // namespace ui + +namespace views { + +namespace internal { +// Internal Edit class that keeps track of edits for undo/redo. +class Edit; + +// C++ doesn't allow forward decl enum, so let's define here. +enum MergeType { + // The edit should not be merged with next edit. It still may + // be merged with an edit with MERGE_WITH_PREVIOUS. + DO_NOT_MERGE, + // The edit can be merged with next edit when possible. + MERGEABLE, + // Does the edit have to be merged with previous edit? + // This forces the merge even if the previous edit is marked + // as DO_NOT_MERGE. + MERGE_WITH_PREVIOUS, +}; + +} // namespace internal + +// A model that represents a text content for TextfieldViews. +// It supports editing, selection and cursor manipulation. +class VIEWS_EXPORT TextfieldViewsModel { + public: + // Delegate interface implemented by the textfield view class to provided + // additional functionalities required by the model. + class VIEWS_EXPORT Delegate { + public: + // Called when the current composition text is confirmed or cleared. + virtual void OnCompositionTextConfirmedOrCleared() = 0; + + protected: + virtual ~Delegate(); + }; + + explicit TextfieldViewsModel(Delegate* delegate); + virtual ~TextfieldViewsModel(); + + void set_is_password(bool is_password) { + is_password_ = is_password; + } + + // Edit related methods. + + const string16& GetText() const; + // Sets the text. Returns true if the text has been modified. The + // current composition text will be confirmed first. Setting + // the same text will not add edit history because it's not user + // visible change nor user-initiated change. This allow a client + // code to set the same text multiple times without worrying about + // messing edit history. + bool SetText(const string16& text); + + gfx::RenderText* render_text() { return render_text_.get(); } + + // Inserts given |text| at the current cursor position. + // The current composition text will be cleared. + void InsertText(const string16& text) { + InsertTextInternal(text, false); + } + + // Inserts a character at the current cursor position. + void InsertChar(char16 c) { + InsertTextInternal(string16(&c, 1), true); + } + + // Replaces characters at the current position with characters in given text. + // The current composition text will be cleared. + void ReplaceText(const string16& text) { + ReplaceTextInternal(text, false); + } + + // Replaces the char at the current position with given character. + void ReplaceChar(char16 c) { + ReplaceTextInternal(string16(&c, 1), true); + } + + // Appends the text. + // The current composition text will be confirmed. + void Append(const string16& text); + + // Deletes the first character after the current cursor position (as if, the + // the user has pressed delete key in the textfield). Returns true if + // the deletion is successful. + // If there is composition text, it'll be deleted instead. + bool Delete(); + + // Deletes the first character before the current cursor position (as if, the + // the user has pressed backspace key in the textfield). Returns true if + // the removal is successful. + // If there is composition text, it'll be deleted instead. + bool Backspace(); + + // Cursor related methods. + + // Returns the current cursor position. + size_t GetCursorPosition() const; + + // Moves the cursor, see RenderText for additional details. + // The current composition text will be confirmed. + void MoveCursorLeft(gfx::BreakType break_type, bool select); + void MoveCursorRight(gfx::BreakType break_type, bool select); + + // Moves the selection to the specified selection in |selection|. + // If there is composition text, it will be confirmed, which will update the + // selection range, and it overrides the selection_start to which the + // selection will move to. + bool MoveCursorTo(const gfx::SelectionModel& selection); + + // Helper function to call MoveCursorTo on the TextfieldViewsModel. + bool MoveCursorTo(const gfx::Point& point, bool select); + + // Selection related method + + // Returns the selected text. + string16 GetSelectedText() const; + + // Gets the selected range. + void GetSelectedRange(ui::Range* range) const; + + // The current composition text will be confirmed. The selection starts with + // the range's start position, and ends with the range's end position, + // therefore the cursor position becomes the end position. + void SelectRange(const ui::Range& range); + + void GetSelectionModel(gfx::SelectionModel* sel) const; + + // The current composition text will be confirmed. + // render_text_'s selection model is set to |sel|. + void SelectSelectionModel(const gfx::SelectionModel& sel); + + // Selects all text. + // The current composition text will be confirmed. + void SelectAll(); + + // Selects the word at which the cursor is currently positioned. + // The current composition text will be confirmed. + void SelectWord(); + + // Clears selection. + // The current composition text will be confirmed. + void ClearSelection(); + + // Returns true if there is an undoable edit. + bool CanUndo(); + + // Returns true if there is an redoable edit. + bool CanRedo(); + + // Undo edit. Returns true if undo changed the text. + bool Undo(); + + // Redo edit. Returns true if redo changed the text. + bool Redo(); + + // Returns visible text. If the field is password, it returns the + // sequence of "*". + string16 GetVisibleText() const; + + // Cuts the currently selected text and puts it to clipboard. Returns true + // if text has changed after cutting. + bool Cut(); + + // Copies the currently selected text and puts it to clipboard. + void Copy(); + + // Pastes text from the clipboard at current cursor position. Returns true + // if text has changed after pasting. + bool Paste(); + + // Tells if any text is selected, even if the selection is in composition + // text. + bool HasSelection() const; + + // Deletes the selected text. This method shouldn't be called with + // composition text. + void DeleteSelection(); + + // Deletes the selected text (if any) and insert text at given + // position. + void DeleteSelectionAndInsertTextAt( + const string16& text, size_t position); + + // Retrieves the text content in a given range. + string16 GetTextFromRange(const ui::Range& range) const; + + // Retrieves the range containing all text in the model. + void GetTextRange(ui::Range* range) const; + + // Sets composition text and attributes. If there is composition text already, + // it’ll be replaced by the new one. Otherwise, current selection will be + // replaced. If there is no selection, the composition text will be inserted + // at the insertion point. + // Any changes to the model except text insertion will confirm the current + // composition text. + void SetCompositionText(const ui::CompositionText& composition); + + // Converts current composition text into final content. + void ConfirmCompositionText(); + + // Removes current composition text. + void CancelCompositionText(); + + // Retrieves the range of current composition text. + void GetCompositionTextRange(ui::Range* range) const; + + // Returns true if there is composition text. + bool HasCompositionText() const; + + private: + friend class NativeTextfieldViews; + friend class NativeTextfieldViewsTest; + friend class TextfieldViewsModelTest; + friend class UndoRedo_BasicTest; + friend class UndoRedo_CutCopyPasteTest; + friend class UndoRedo_ReplaceTest; + friend class internal::Edit; + + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_BasicTest); + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_CutCopyPasteTest); + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_ReplaceTest); + + // Returns the visible text given |start| and |end|. + string16 GetVisibleText(size_t start, size_t end) const; + + // Insert the given |text|. |mergeable| indicates if this insert + // operation can be merged to previous edit in the edit history. + void InsertTextInternal(const string16& text, bool mergeable); + + // Replace the current text with the given |text|. |mergeable| + // indicates if this replace operation can be merged to previous + // edit in the edit history. + void ReplaceTextInternal(const string16& text, bool mergeable); + + // Clears all edit history. + void ClearEditHistory(); + + // Clears redo history. + void ClearRedoHistory(); + + // Executes and records edit operations. + void ExecuteAndRecordDelete(size_t from, size_t to, bool mergeable); + void ExecuteAndRecordReplaceSelection(internal::MergeType merge_type, + const string16& text); + void ExecuteAndRecordReplace(internal::MergeType merge_type, + size_t old_cursor_pos, + size_t new_cursor_pos, + const string16& text, + size_t new_text_start); + void ExecuteAndRecordInsert(const string16& text, bool mergeable); + + // Adds or merge |edit| into edit history. Return true if the edit + // has been merged and must be deleted after redo. + bool AddOrMergeEditHistory(internal::Edit* edit); + + // Modify the text buffer in following way: + // 1) Delete the string from |delete_from| to |delte_to|. + // 2) Insert the |new_text| at the index |new_text_insert_at|. + // Note that the index is after deletion. + // 3) Move the cursor to |new_cursor_pos|. + void ModifyText(size_t delete_from, + size_t delete_to, + const string16& new_text, + size_t new_text_insert_at, + size_t new_cursor_pos); + + void ClearComposition(); + + // Pointer to a TextfieldViewsModel::Delegate instance, should be provided by + // the View object. + Delegate* delegate_; + + // The stylized text, cursor, selection, and the visual layout model. + scoped_ptr<gfx::RenderText> render_text_; + + // True if the text is the password. + bool is_password_; + + typedef std::list<internal::Edit*> EditHistory; + EditHistory edit_history_; + + // An iterator that points to the current edit that can be undone. + // This iterator moves from the |end()|, meaining no edit to undo, + // to the last element (one before |end()|), meaning no edit to redo. + // There is no edit to undo (== end()) when: + // 1) in initial state. (nothing to undo) + // 2) very 1st edit is undone. + // 3) all edit history is removed. + // There is no edit to redo (== last element or no element) when: + // 1) in initial state. (nothing to redo) + // 2) new edit is added. (redo history is cleared) + // 3) redone all undone edits. + EditHistory::iterator current_edit_; + + DISALLOW_COPY_AND_ASSIGN(TextfieldViewsModel); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_VIEWS_MODEL_H_ diff --git a/ui/views/controls/textfield/textfield_views_model_unittest.cc b/ui/views/controls/textfield/textfield_views_model_unittest.cc new file mode 100644 index 0000000..1cf7d71 --- /dev/null +++ b/ui/views/controls/textfield/textfield_views_model_unittest.cc @@ -0,0 +1,1535 @@ +// 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 <vector> + +#include "base/auto_reset.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "base/utf_string_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/clipboard/scoped_clipboard_writer.h" +#include "ui/base/range/range.h" +#include "ui/gfx/render_text.h" +#include "ui/views/controls/textfield/textfield.h" +#include "ui/views/controls/textfield/textfield_views_model.h" +#include "ui/views/test/test_views_delegate.h" +#include "ui/views/test/views_test_base.h" +#include "views/views_delegate.h" + +namespace { + +struct WordAndCursor { + WordAndCursor(const wchar_t* w, size_t c) + : word(w), + cursor(c) { + } + + const wchar_t* word; + size_t cursor; +}; + +} // namespace + +namespace views { + +class TextfieldViewsModelTest : public ViewsTestBase, + public TextfieldViewsModel::Delegate { + public: + TextfieldViewsModelTest() + : ViewsTestBase(), + composition_text_confirmed_or_cleared_(false) { + } + + virtual void OnCompositionTextConfirmedOrCleared() { + composition_text_confirmed_or_cleared_ = true; + } + + protected: + void ResetModel(TextfieldViewsModel* model) const { + model->SetText(ASCIIToUTF16("")); + model->ClearEditHistory(); + } + + bool composition_text_confirmed_or_cleared_; + + private: + DISALLOW_COPY_AND_ASSIGN(TextfieldViewsModelTest); +}; + +#define EXPECT_STR_EQ(ascii, utf16) \ + EXPECT_EQ(ASCIIToWide(ascii), UTF16ToWide(utf16)) +TEST_F(TextfieldViewsModelTest, EditString) { + TextfieldViewsModel model(NULL); + // append two strings + model.Append(ASCIIToUTF16("HILL")); + EXPECT_STR_EQ("HILL", model.GetText()); + model.Append(ASCIIToUTF16("WORLD")); + EXPECT_STR_EQ("HILLWORLD", model.GetText()); + + // Insert "E" to make hello + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + model.InsertChar('E'); + EXPECT_STR_EQ("HEILLWORLD", model.GetText()); + // Replace "I" with "L" + model.ReplaceChar('L'); + EXPECT_STR_EQ("HELLLWORLD", model.GetText()); + model.ReplaceChar('L'); + model.ReplaceChar('O'); + EXPECT_STR_EQ("HELLOWORLD", model.GetText()); + + // Delete 6th char "W", then delete 5th char O" + EXPECT_EQ(5U, model.GetCursorPosition()); + EXPECT_TRUE(model.Delete()); + EXPECT_STR_EQ("HELLOORLD", model.GetText()); + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(4U, model.GetCursorPosition()); + EXPECT_STR_EQ("HELLORLD", model.GetText()); + + // Move the cursor to start. backspace should fail. + model.MoveCursorLeft(gfx::LINE_BREAK, false); + EXPECT_FALSE(model.Backspace()); + EXPECT_STR_EQ("HELLORLD", model.GetText()); + // Move the cursor to the end. delete should fail. + model.MoveCursorRight(gfx::LINE_BREAK, false); + EXPECT_FALSE(model.Delete()); + EXPECT_STR_EQ("HELLORLD", model.GetText()); + // but backspace should work. + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("HELLORL", model.GetText()); + + model.MoveCursorTo(gfx::SelectionModel(5)); + model.ReplaceText(ASCIIToUTF16(" WOR")); + EXPECT_STR_EQ("HELLO WORL", model.GetText()); +} + +TEST_F(TextfieldViewsModelTest, EditString_SimpleRTL) { + TextfieldViewsModel model(NULL); + // Append two strings. + model.Append(WideToUTF16(L"\x05d0\x05d1\x05d2")); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05d1\x05d2"), model.GetText()); + model.Append(WideToUTF16(L"\x05e0\x05e1\x05e2")); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05d1\x05d2\x05e0\x05e1\x05e2"), + model.GetText()); + + // Insert 0x05f0. + model.MoveCursorTo(gfx::SelectionModel(1U)); + model.InsertChar(0x05f0); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05f0\x05d1\x05d2\x05e0\x05e1\x05e2"), + model.GetText()); + + // Replace "\x05d1" with "\x05f1". + model.ReplaceChar(0x05f1); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05f0\x5f1\x05d2\x05e0\x05e1\x05e2"), + model.GetText()); + + // Delete and backspace. + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Delete()); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05f0\x5f1\x05e0\x05e1\x05e2"), + model.GetText()); + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(2U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05f0\x05e0\x05e1\x05e2"), model.GetText()); +} + +TEST_F(TextfieldViewsModelTest, EditString_ComplexScript) { + TextfieldViewsModel model(NULL); + // Append two Hindi strings. + model.Append(WideToUTF16(L"\x0915\x093f\x0915\x094d\x0915")); + EXPECT_EQ(WideToUTF16(L"\x0915\x093f\x0915\x094d\x0915"), + model.GetText()); + model.Append(WideToUTF16(L"\x0915\x094d\x092e\x094d")); + EXPECT_EQ(WideToUTF16( + L"\x0915\x093f\x0915\x094d\x0915\x0915\x094d\x092e\x094d"), + model.GetText()); + + // Check it is not able to place cursor in middle of a grapheme. + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + model.MoveCursorTo(gfx::SelectionModel(1U)); + EXPECT_EQ(0U, model.GetCursorPosition()); +#endif + + model.MoveCursorTo(gfx::SelectionModel(2U)); + EXPECT_EQ(2U, model.GetCursorPosition()); + model.InsertChar('a'); + EXPECT_EQ(WideToUTF16( + L"\x0915\x093f\x0061\x0915\x094d\x0915\x0915\x094d\x092e\x094d"), + model.GetText()); + + // ReplaceChar will replace the whole grapheme. + model.ReplaceChar('b'); + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + EXPECT_EQ(WideToUTF16( + L"\x0915\x093f\x0061\x0062\x0915\x0915\x094d\x092e\x094d"), + model.GetText()); +#endif + EXPECT_EQ(4U, model.GetCursorPosition()); + + // Delete should delete the whole grapheme. + model.MoveCursorTo(gfx::SelectionModel(0U)); + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + EXPECT_TRUE(model.Delete()); + EXPECT_EQ(WideToUTF16(L"\x0061\x0062\x0915\x0915\x094d\x092e\x094d"), + model.GetText()); + model.MoveCursorTo(gfx::SelectionModel(model.GetText().length())); + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(WideToUTF16(L"\x0061\x0062\x0915\x0915\x094d\x092e"), + model.GetText()); +#endif + + // Test cursor position and deletion for Hindi Virama. + model.SetText(WideToUTF16(L"\x0D38\x0D4D\x0D15\x0D16\x0D2E")); + model.MoveCursorTo(gfx::SelectionModel(0)); + EXPECT_EQ(0U, model.GetCursorPosition()); + + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + model.MoveCursorTo(gfx::SelectionModel(1)); + EXPECT_EQ(0U, model.GetCursorPosition()); +#endif + + model.MoveCursorTo(gfx::SelectionModel(2)); + EXPECT_EQ(2U, model.GetCursorPosition()); + + model.MoveCursorTo(gfx::SelectionModel(3)); + EXPECT_EQ(3U, model.GetCursorPosition()); + + model.MoveCursorTo(gfx::SelectionModel(2)); + + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(WideToUTF16(L"\x0D38\x0D15\x0D16\x0D2E"), model.GetText()); + + // Test Delete/Backspace on Hebrew with non-spacing marks. + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + model.SetText(WideToUTF16(L"\x05d5\x05b7\x05D9\x05B0\x05D4\x05B4\x05D9")); + model.MoveCursorTo(gfx::SelectionModel(0)); + EXPECT_TRUE(model.Delete()); + EXPECT_TRUE(model.Delete()); + EXPECT_TRUE(model.Delete()); + EXPECT_TRUE(model.Delete()); + EXPECT_EQ(WideToUTF16(L""), model.GetText()); +#endif + + // The first 2 characters are not strong directionality characters. + model.SetText(WideToUTF16(L"\x002C\x0020\x05D1\x05BC\x05B7\x05E9\x05BC")); +#if defined(OS_WIN) + model.MoveCursorRight(gfx::LINE_BREAK, false); +#else + model.MoveCursorLeft(gfx::LINE_BREAK, false); +#endif + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(WideToUTF16(L"\x002C\x0020\x05D1\x05BC\x05B7\x05E9"), + model.GetText()); +} + +TEST_F(TextfieldViewsModelTest, EmptyString) { + TextfieldViewsModel model(NULL); + EXPECT_EQ(string16(), model.GetText()); + EXPECT_EQ(string16(), model.GetSelectedText()); + EXPECT_EQ(string16(), model.GetVisibleText()); + + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(0U, model.GetCursorPosition()); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(0U, model.GetCursorPosition()); + + EXPECT_EQ(string16(), model.GetSelectedText()); + + EXPECT_FALSE(model.Delete()); + EXPECT_FALSE(model.Backspace()); +} + +TEST_F(TextfieldViewsModelTest, Selection) { + TextfieldViewsModel model(NULL); + model.Append(ASCIIToUTF16("HELLO")); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_STR_EQ("E", model.GetSelectedText()); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_STR_EQ("EL", model.GetSelectedText()); + + model.MoveCursorLeft(gfx::LINE_BREAK, true); + EXPECT_STR_EQ("H", model.GetSelectedText()); + model.MoveCursorRight(gfx::LINE_BREAK, true); + EXPECT_STR_EQ("ELLO", model.GetSelectedText()); + model.ClearSelection(); + EXPECT_EQ(string16(), model.GetSelectedText()); + model.SelectAll(); + EXPECT_STR_EQ("HELLO", model.GetSelectedText()); + // SelectAll should select towards the end. + gfx::SelectionModel sel; + model.GetSelectionModel(&sel); + EXPECT_EQ(0U, sel.selection_start()); + EXPECT_EQ(5U, sel.selection_end()); + + // Select and move cursor + model.SelectRange(ui::Range(1U, 3U)); + EXPECT_STR_EQ("EL", model.GetSelectedText()); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, false); + EXPECT_EQ(1U, model.GetCursorPosition()); + model.SelectRange(ui::Range(1U, 3U)); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + EXPECT_EQ(3U, model.GetCursorPosition()); + + // Select all and move cursor + model.SelectAll(); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, false); + EXPECT_EQ(0U, model.GetCursorPosition()); + model.SelectAll(); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + EXPECT_EQ(5U, model.GetCursorPosition()); +} + +TEST_F(TextfieldViewsModelTest, Selection_BidiWithNonSpacingMarks) { + // Selection is a logical operation. And it should work with the arrow + // keys doing visual movements, while the selection is logical between + // the (logical) start and end points. Selection is simply defined as + // the portion of text between the logical positions of the start and end + // caret positions. + TextfieldViewsModel model(NULL); + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + model.Append(WideToUTF16( + L"abc\x05E9\x05BC\x05C1\x05B8\x05E0\x05B8"L"def")); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(2U, model.render_text()->GetSelectionStart()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"c"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(2U, model.render_text()->GetSelectionStart()); + EXPECT_EQ(7U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"c\x05E9\x05BC\x05C1\x05B8"), + model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(2U, model.render_text()->GetSelectionStart()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"c"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(2U, model.render_text()->GetSelectionStart()); + EXPECT_EQ(10U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"c\x05E9\x05BC\x05C1\x05B8\x05E0\x05B8"L"d"), + model.GetSelectedText()); + + model.ClearSelection(); + EXPECT_EQ(string16(), model.GetSelectedText()); + model.SelectAll(); + EXPECT_EQ(WideToUTF16(L"abc\x05E9\x05BC\x05C1\x05B8\x05E0\x05B8"L"def"), + model.GetSelectedText()); +#endif + + // In case of "aBc", this test shows how to select "aB" or "Bc", assume 'B' is + // an RTL character. + model.SetText(WideToUTF16(L"a\x05E9"L"b")); + model.MoveCursorTo(gfx::SelectionModel(0)); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a\x05E9"L"b"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + EXPECT_EQ(3U, model.GetCursorPosition()); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"b"), model.GetSelectedText()); + + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"b"), model.GetSelectedText()); + + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a\x05E9"L"b"), model.GetSelectedText()); + + model.MoveCursorLeft(gfx::LINE_BREAK, false); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a\x05E9"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"\x05E9"L"b"), model.GetSelectedText()); + + model.ClearSelection(); + EXPECT_EQ(string16(), model.GetSelectedText()); + model.SelectAll(); + EXPECT_EQ(WideToUTF16(L"a\x05E9"L"b"), model.GetSelectedText()); +} + +TEST_F(TextfieldViewsModelTest, SelectionAndEdit) { + TextfieldViewsModel model(NULL); + model.Append(ASCIIToUTF16("HELLO")); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); // select "EL" + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("HLO", model.GetText()); + + model.Append(ASCIIToUTF16("ILL")); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); // select "LO" + EXPECT_TRUE(model.Delete()); + EXPECT_STR_EQ("HILL", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); // select "I" + model.InsertChar('E'); + EXPECT_STR_EQ("HELL", model.GetText()); + model.MoveCursorLeft(gfx::LINE_BREAK, false); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); // select "H" + model.ReplaceChar('B'); + EXPECT_STR_EQ("BELL", model.GetText()); + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); // select "ELL" + model.ReplaceChar('E'); + EXPECT_STR_EQ("BEE", model.GetText()); +} + +TEST_F(TextfieldViewsModelTest, Password) { + TextfieldViewsModel model(NULL); + model.set_is_password(true); + model.Append(ASCIIToUTF16("HELLO")); + EXPECT_STR_EQ("*****", model.GetVisibleText()); + EXPECT_STR_EQ("HELLO", model.GetText()); + EXPECT_TRUE(model.Delete()); + + EXPECT_STR_EQ("****", model.GetVisibleText()); + EXPECT_STR_EQ("ELLO", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + + model.SelectAll(); + EXPECT_STR_EQ("ELLO", model.GetSelectedText()); + EXPECT_EQ(4U, model.GetCursorPosition()); + + model.InsertChar('X'); + EXPECT_STR_EQ("*", model.GetVisibleText()); + EXPECT_STR_EQ("X", model.GetText()); +} + +TEST_F(TextfieldViewsModelTest, Word) { + TextfieldViewsModel model(NULL); + model.Append( + ASCIIToUTF16("The answer to Life, the Universe, and Everything")); + model.MoveCursorRight(gfx::WORD_BREAK, false); + EXPECT_EQ(3U, model.GetCursorPosition()); + model.MoveCursorRight(gfx::WORD_BREAK, false); + EXPECT_EQ(10U, model.GetCursorPosition()); + model.MoveCursorRight(gfx::WORD_BREAK, false); + model.MoveCursorRight(gfx::WORD_BREAK, false); + EXPECT_EQ(18U, model.GetCursorPosition()); + + // Should passes the non word char ',' + model.MoveCursorRight(gfx::WORD_BREAK, true); + EXPECT_EQ(23U, model.GetCursorPosition()); + EXPECT_STR_EQ(", the", model.GetSelectedText()); + + // Move to the end. + model.MoveCursorRight(gfx::WORD_BREAK, true); + model.MoveCursorRight(gfx::WORD_BREAK, true); + model.MoveCursorRight(gfx::WORD_BREAK, true); + EXPECT_STR_EQ(", the Universe, and Everything", model.GetSelectedText()); + // Should be safe to go next word at the end. + model.MoveCursorRight(gfx::WORD_BREAK, true); + EXPECT_STR_EQ(", the Universe, and Everything", model.GetSelectedText()); + model.InsertChar('2'); + EXPECT_EQ(19U, model.GetCursorPosition()); + + // Now backwards. + model.MoveCursorLeft(gfx::CHARACTER_BREAK, false); // leave 2. + model.MoveCursorLeft(gfx::WORD_BREAK, true); + EXPECT_EQ(14U, model.GetCursorPosition()); + EXPECT_STR_EQ("Life", model.GetSelectedText()); + model.MoveCursorLeft(gfx::WORD_BREAK, true); + EXPECT_STR_EQ("to Life", model.GetSelectedText()); + model.MoveCursorLeft(gfx::WORD_BREAK, true); + model.MoveCursorLeft(gfx::WORD_BREAK, true); + model.MoveCursorLeft(gfx::WORD_BREAK, true); // Select to the begining. + EXPECT_STR_EQ("The answer to Life", model.GetSelectedText()); + // Should be safe to go pervious word at the begining. + model.MoveCursorLeft(gfx::WORD_BREAK, true); + EXPECT_STR_EQ("The answer to Life", model.GetSelectedText()); + model.ReplaceChar('4'); + EXPECT_EQ(string16(), model.GetSelectedText()); + EXPECT_STR_EQ("42", model.GetVisibleText()); +} + +TEST_F(TextfieldViewsModelTest, SetText) { + TextfieldViewsModel model(NULL); + model.Append(ASCIIToUTF16("HELLO")); + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.SetText(ASCIIToUTF16("GOODBYE")); + EXPECT_STR_EQ("GOODBYE", model.GetText()); + // SetText won't reset the cursor posistion. + EXPECT_EQ(5U, model.GetCursorPosition()); + model.SelectAll(); + EXPECT_STR_EQ("GOODBYE", model.GetSelectedText()); + model.MoveCursorRight(gfx::LINE_BREAK, false); + EXPECT_EQ(7U, model.GetCursorPosition()); + + model.SetText(ASCIIToUTF16("BYE")); + // Setting shorter string moves the cursor to the end of the new string. + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_EQ(string16(), model.GetSelectedText()); + model.SetText(ASCIIToUTF16("")); + EXPECT_EQ(0U, model.GetCursorPosition()); +} + +#if defined(USE_AURA) && defined(OS_LINUX) +#define MAYBE_Clipboard DISABLED_Clipboard // http://crbug.com/97845 +#else +#define MAYBE_Clipboard Clipboard +#endif +TEST_F(TextfieldViewsModelTest, MAYBE_Clipboard) { + ui::Clipboard* clipboard + = views::ViewsDelegate::views_delegate->GetClipboard(); + string16 initial_clipboard_text = ASCIIToUTF16("initial text"); + ui::ScopedClipboardWriter(clipboard).WriteText(initial_clipboard_text); + + string16 clipboard_text; + TextfieldViewsModel model(NULL); + model.Append(ASCIIToUTF16("HELLO WORLD")); + model.MoveCursorRight(gfx::LINE_BREAK, false); + + // Test for cut: Empty selection. + EXPECT_FALSE(model.Cut()); + clipboard->ReadText(ui::Clipboard::BUFFER_STANDARD, &clipboard_text); + EXPECT_STR_EQ(UTF16ToUTF8(initial_clipboard_text), clipboard_text); + EXPECT_STR_EQ("HELLO WORLD", model.GetText()); + EXPECT_EQ(11U, model.GetCursorPosition()); + + // Test for cut: Non-empty selection. + model.MoveCursorLeft(gfx::WORD_BREAK, true); + EXPECT_TRUE(model.Cut()); + clipboard->ReadText(ui::Clipboard::BUFFER_STANDARD, &clipboard_text); + EXPECT_STR_EQ("WORLD", clipboard_text); + EXPECT_STR_EQ("HELLO ", model.GetText()); + EXPECT_EQ(6U, model.GetCursorPosition()); + + // Test for copy: Empty selection. + model.Copy(); + clipboard->ReadText(ui::Clipboard::BUFFER_STANDARD, &clipboard_text); + EXPECT_STR_EQ("WORLD", clipboard_text); + EXPECT_STR_EQ("HELLO ", model.GetText()); + EXPECT_EQ(6U, model.GetCursorPosition()); + + // Test for copy: Non-empty selection. + model.Append(ASCIIToUTF16("HELLO WORLD")); + model.SelectAll(); + model.Copy(); + clipboard->ReadText(ui::Clipboard::BUFFER_STANDARD, &clipboard_text); + EXPECT_STR_EQ("HELLO HELLO WORLD", clipboard_text); + EXPECT_STR_EQ("HELLO HELLO WORLD", model.GetText()); + EXPECT_EQ(17U, model.GetCursorPosition()); + + // Test for paste. + model.ClearSelection(); + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.MoveCursorLeft(gfx::WORD_BREAK, true); + EXPECT_TRUE(model.Paste()); + clipboard->ReadText(ui::Clipboard::BUFFER_STANDARD, &clipboard_text); + EXPECT_STR_EQ("HELLO HELLO WORLD", clipboard_text); + EXPECT_STR_EQ("HELLO HELLO HELLO HELLO WORLD", model.GetText()); + EXPECT_EQ(29U, model.GetCursorPosition()); +} + +void SelectWordTestVerifier(TextfieldViewsModel &model, + const string16 &expected_selected_string, size_t expected_cursor_pos) { + EXPECT_EQ(expected_selected_string, model.GetSelectedText()); + EXPECT_EQ(expected_cursor_pos, model.GetCursorPosition()); +} + +TEST_F(TextfieldViewsModelTest, SelectWordTest) { + TextfieldViewsModel model(NULL); + model.Append(ASCIIToUTF16(" HELLO !! WO RLD ")); + + // Test when cursor is at the beginning. + model.MoveCursorLeft(gfx::LINE_BREAK, false); + model.SelectWord(); + SelectWordTestVerifier(model, ASCIIToUTF16(" "), 2U); + + // Test when cursor is at the beginning of a word. + gfx::SelectionModel selection(2U); + model.MoveCursorTo(selection); + model.SelectWord(); + SelectWordTestVerifier(model, ASCIIToUTF16("HELLO"), 7U); + + // Test when cursor is at the end of a word. + selection = gfx::SelectionModel(15U); + model.MoveCursorTo(selection); + model.SelectWord(); + SelectWordTestVerifier(model, ASCIIToUTF16(" "), 20U); + + // Test when cursor is somewhere in a non-alpha-numeric fragment. + for (size_t cursor_pos = 8; cursor_pos < 13U; cursor_pos++) { + selection = gfx::SelectionModel(cursor_pos); + model.MoveCursorTo(selection); + model.SelectWord(); + SelectWordTestVerifier(model, ASCIIToUTF16(" !! "), 13U); + } + + // Test when cursor is somewhere in a whitespace fragment. + selection = gfx::SelectionModel(17U); + model.MoveCursorTo(selection); + model.SelectWord(); + SelectWordTestVerifier(model, ASCIIToUTF16(" "), 20U); + + // Test when cursor is at the end. + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.SelectWord(); + SelectWordTestVerifier(model, ASCIIToUTF16(" "), 24U); +} + +// TODO(xji): temporarily disable in platform Win since the complex script +// characters and Chinese characters are turned into empty square due to font +// regression. +#if defined(OS_LINUX) +TEST_F(TextfieldViewsModelTest, SelectWordTest_MixScripts) { + TextfieldViewsModel model(NULL); + std::vector<WordAndCursor> word_and_cursor; + word_and_cursor.push_back(WordAndCursor(L"a\x05d0", 2)); + word_and_cursor.push_back(WordAndCursor(L"a\x05d0", 2)); + word_and_cursor.push_back(WordAndCursor(L"\x05d1\x05d2", 5)); + word_and_cursor.push_back(WordAndCursor(L"\x05d1\x05d2", 5)); + word_and_cursor.push_back(WordAndCursor(L" ", 3)); + word_and_cursor.push_back(WordAndCursor(L"a\x05d0", 2)); + word_and_cursor.push_back(WordAndCursor(L"\x0915\x094d\x0915", 9)); + word_and_cursor.push_back(WordAndCursor(L"\x0915\x094d\x0915", 9)); + word_and_cursor.push_back(WordAndCursor(L" ", 10)); + word_and_cursor.push_back(WordAndCursor(L"\x4E2D\x56FD", 12)); + word_and_cursor.push_back(WordAndCursor(L"\x4E2D\x56FD", 12)); + word_and_cursor.push_back(WordAndCursor(L"\x82B1", 13)); + word_and_cursor.push_back(WordAndCursor(L"\x5929", 14)); + + // The text consists of Ascii, Hebrew, Hindi with Virama sign, and Chinese. + model.SetText(WideToUTF16(L"a\x05d0 \x05d1\x05d2 \x0915\x094d\x0915 " + L"\x4E2D\x56FD\x82B1\x5929")); + for (size_t i = 0; i < word_and_cursor.size(); ++i) { + model.MoveCursorLeft(gfx::LINE_BREAK, false); + for (size_t j = 0; j < i; ++j) + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + model.SelectWord(); + SelectWordTestVerifier(model, WideToUTF16(word_and_cursor[i].word), + word_and_cursor[i].cursor); + } +} +#endif + +TEST_F(TextfieldViewsModelTest, RangeTest) { + TextfieldViewsModel model(NULL); + model.Append(ASCIIToUTF16("HELLO WORLD")); + model.MoveCursorLeft(gfx::LINE_BREAK, false); + ui::Range range; + model.GetSelectedRange(&range); + EXPECT_TRUE(range.is_empty()); + EXPECT_EQ(0U, range.start()); + EXPECT_EQ(0U, range.end()); + + model.MoveCursorRight(gfx::WORD_BREAK, true); + model.GetSelectedRange(&range); + EXPECT_FALSE(range.is_empty()); + EXPECT_FALSE(range.is_reversed()); + EXPECT_EQ(0U, range.start()); + EXPECT_EQ(5U, range.end()); + + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + model.GetSelectedRange(&range); + EXPECT_FALSE(range.is_empty()); + EXPECT_EQ(0U, range.start()); + EXPECT_EQ(4U, range.end()); + + model.MoveCursorLeft(gfx::WORD_BREAK, true); + model.GetSelectedRange(&range); + EXPECT_TRUE(range.is_empty()); + EXPECT_EQ(0U, range.start()); + EXPECT_EQ(0U, range.end()); + + // now from the end. + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.GetSelectedRange(&range); + EXPECT_TRUE(range.is_empty()); + EXPECT_EQ(11U, range.start()); + EXPECT_EQ(11U, range.end()); + + model.MoveCursorLeft(gfx::WORD_BREAK, true); + model.GetSelectedRange(&range); + EXPECT_FALSE(range.is_empty()); + EXPECT_TRUE(range.is_reversed()); + EXPECT_EQ(11U, range.start()); + EXPECT_EQ(6U, range.end()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + model.GetSelectedRange(&range); + EXPECT_FALSE(range.is_empty()); + EXPECT_TRUE(range.is_reversed()); + EXPECT_EQ(11U, range.start()); + EXPECT_EQ(7U, range.end()); + + model.MoveCursorRight(gfx::WORD_BREAK, true); + model.GetSelectedRange(&range); + EXPECT_TRUE(range.is_empty()); + EXPECT_EQ(11U, range.start()); + EXPECT_EQ(11U, range.end()); + + // Select All + model.MoveCursorLeft(gfx::LINE_BREAK, true); + model.GetSelectedRange(&range); + EXPECT_FALSE(range.is_empty()); + EXPECT_TRUE(range.is_reversed()); + EXPECT_EQ(11U, range.start()); + EXPECT_EQ(0U, range.end()); +} + +TEST_F(TextfieldViewsModelTest, SelectRangeTest) { + TextfieldViewsModel model(NULL); + model.Append(ASCIIToUTF16("HELLO WORLD")); + ui::Range range(0, 6); + EXPECT_FALSE(range.is_reversed()); + model.SelectRange(range); + EXPECT_STR_EQ("HELLO ", model.GetSelectedText()); + + range = ui::Range(6, 1); + EXPECT_TRUE(range.is_reversed()); + model.SelectRange(range); + EXPECT_STR_EQ("ELLO ", model.GetSelectedText()); + + range = ui::Range(2, 1000); + EXPECT_FALSE(range.is_reversed()); + model.SelectRange(range); + EXPECT_STR_EQ("LLO WORLD", model.GetSelectedText()); + + range = ui::Range(1000, 3); + EXPECT_TRUE(range.is_reversed()); + model.SelectRange(range); + EXPECT_STR_EQ("LO WORLD", model.GetSelectedText()); + + range = ui::Range(0, 0); + EXPECT_TRUE(range.is_empty()); + model.SelectRange(range); + EXPECT_TRUE(model.GetSelectedText().empty()); + + range = ui::Range(3, 3); + EXPECT_TRUE(range.is_empty()); + model.SelectRange(range); + EXPECT_TRUE(model.GetSelectedText().empty()); + + range = ui::Range(1000, 100); + EXPECT_FALSE(range.is_empty()); + model.SelectRange(range); + EXPECT_TRUE(model.GetSelectedText().empty()); + + range = ui::Range(1000, 1000); + EXPECT_TRUE(range.is_empty()); + model.SelectRange(range); + EXPECT_TRUE(model.GetSelectedText().empty()); +} + +TEST_F(TextfieldViewsModelTest, SelectionModelTest) { + TextfieldViewsModel model(NULL); + model.Append(ASCIIToUTF16("HELLO WORLD")); + model.MoveCursorLeft(gfx::LINE_BREAK, false); + gfx::SelectionModel sel; + model.GetSelectionModel(&sel); + EXPECT_EQ(sel.selection_start(), sel.selection_end()); + EXPECT_EQ(0U, sel.selection_start()); + EXPECT_EQ(0U, sel.selection_end()); + + model.MoveCursorRight(gfx::WORD_BREAK, true); + model.GetSelectionModel(&sel); + EXPECT_NE(sel.selection_start(), sel.selection_end()); + EXPECT_LE(sel.selection_start(), sel.selection_end()); + EXPECT_EQ(0U, sel.selection_start()); + EXPECT_EQ(5U, sel.selection_end()); + + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + model.GetSelectionModel(&sel); + EXPECT_NE(sel.selection_start(), sel.selection_end()); + EXPECT_EQ(0U, sel.selection_start()); + EXPECT_EQ(4U, sel.selection_end()); + + model.MoveCursorLeft(gfx::WORD_BREAK, true); + model.GetSelectionModel(&sel); + EXPECT_EQ(sel.selection_start(), sel.selection_end()); + EXPECT_EQ(0U, sel.selection_start()); + EXPECT_EQ(0U, sel.selection_end()); + + // now from the end. + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.GetSelectionModel(&sel); + EXPECT_EQ(sel.selection_start(), sel.selection_end()); + EXPECT_EQ(11U, sel.selection_start()); + EXPECT_EQ(11U, sel.selection_end()); + + model.MoveCursorLeft(gfx::WORD_BREAK, true); + model.GetSelectionModel(&sel); + EXPECT_NE(sel.selection_start(), sel.selection_end()); + EXPECT_GT(sel.selection_start(), sel.selection_end()); + EXPECT_EQ(11U, sel.selection_start()); + EXPECT_EQ(6U, sel.selection_end()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + model.GetSelectionModel(&sel); + EXPECT_NE(sel.selection_start(), sel.selection_end()); + EXPECT_GT(sel.selection_start(), sel.selection_end()); + EXPECT_EQ(11U, sel.selection_start()); + EXPECT_EQ(7U, sel.selection_end()); + + model.MoveCursorRight(gfx::WORD_BREAK, true); + model.GetSelectionModel(&sel); + EXPECT_EQ(sel.selection_start(), sel.selection_end()); + EXPECT_EQ(11U, sel.selection_start()); + EXPECT_EQ(11U, sel.selection_end()); + + // Select All + model.MoveCursorLeft(gfx::LINE_BREAK, true); + model.GetSelectionModel(&sel); + EXPECT_NE(sel.selection_start(), sel.selection_end()); + EXPECT_GT(sel.selection_start(), sel.selection_end()); + EXPECT_EQ(11U, sel.selection_start()); + EXPECT_EQ(0U, sel.selection_end()); +} + +TEST_F(TextfieldViewsModelTest, SelectSelectionModelTest) { + TextfieldViewsModel model(NULL); + model.Append(ASCIIToUTF16("HELLO WORLD")); + model.SelectSelectionModel(gfx::SelectionModel(0, 6, 5, + gfx::SelectionModel::TRAILING)); + EXPECT_STR_EQ("HELLO ", model.GetSelectedText()); + + model.SelectSelectionModel(gfx::SelectionModel(6, 1, 1, + gfx::SelectionModel::LEADING)); + EXPECT_STR_EQ("ELLO ", model.GetSelectedText()); + + model.SelectSelectionModel(gfx::SelectionModel(2, 1000, 999, + gfx::SelectionModel::TRAILING)); + EXPECT_STR_EQ("LLO WORLD", model.GetSelectedText()); + + model.SelectSelectionModel(gfx::SelectionModel(1000, 3, 3, + gfx::SelectionModel::LEADING)); + EXPECT_STR_EQ("LO WORLD", model.GetSelectedText()); + + model.SelectSelectionModel(gfx::SelectionModel(0, 0, 0, + gfx::SelectionModel::LEADING)); + EXPECT_TRUE(model.GetSelectedText().empty()); + + model.SelectSelectionModel(gfx::SelectionModel(3, 3, 3, + gfx::SelectionModel::LEADING)); + EXPECT_TRUE(model.GetSelectedText().empty()); + + model.SelectSelectionModel(gfx::SelectionModel(1000, 100, 100, + gfx::SelectionModel::LEADING)); + EXPECT_TRUE(model.GetSelectedText().empty()); + + model.SelectSelectionModel(gfx::SelectionModel(1000, 1000, 1000, + gfx::SelectionModel::TRAILING)); + EXPECT_TRUE(model.GetSelectedText().empty()); +} + +TEST_F(TextfieldViewsModelTest, CompositionTextTest) { + TextfieldViewsModel model(this); + model.Append(ASCIIToUTF16("1234590")); + model.SelectRange(ui::Range(5, 5)); + EXPECT_FALSE(model.HasSelection()); + EXPECT_EQ(5U, model.GetCursorPosition()); + + ui::Range range; + model.GetTextRange(&range); + EXPECT_EQ(0U, range.start()); + EXPECT_EQ(7U, range.end()); + + ui::CompositionText composition; + composition.text = ASCIIToUTF16("678"); + composition.underlines.push_back(ui::CompositionUnderline(0, 3, 0, false)); + composition.selection = ui::Range(2, 3); + model.SetCompositionText(composition); + EXPECT_TRUE(model.HasCompositionText()); + EXPECT_TRUE(model.HasSelection()); + + model.GetTextRange(&range); + EXPECT_EQ(10U, range.end()); + EXPECT_STR_EQ("1234567890", model.GetText()); + + model.GetCompositionTextRange(&range); + EXPECT_EQ(5U, range.start()); + EXPECT_EQ(8U, range.end()); + // composition text + EXPECT_STR_EQ("456", model.GetTextFromRange(ui::Range(3, 6))); + + gfx::SelectionModel selection; + model.GetSelectionModel(&selection); + EXPECT_EQ(7U, selection.selection_start()); + EXPECT_EQ(8U, selection.selection_end()); + EXPECT_STR_EQ("8", model.GetSelectedText()); + + EXPECT_FALSE(composition_text_confirmed_or_cleared_); + model.CancelCompositionText(); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_FALSE(model.HasCompositionText()); + EXPECT_FALSE(model.HasSelection()); + EXPECT_EQ(5U, model.GetCursorPosition()); + + model.SetCompositionText(composition); + EXPECT_STR_EQ("1234567890", model.GetText()); + EXPECT_TRUE(model.SetText(ASCIIToUTF16("1234567890"))); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + model.MoveCursorRight(gfx::LINE_BREAK, false); + + model.SetCompositionText(composition); + EXPECT_STR_EQ("1234567890678", model.GetText()); + + model.InsertText(UTF8ToUTF16("-")); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("1234567890-", model.GetText()); + EXPECT_FALSE(model.HasCompositionText()); + EXPECT_FALSE(model.HasSelection()); + + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_STR_EQ("-", model.GetSelectedText()); + model.SetCompositionText(composition); + EXPECT_STR_EQ("1234567890678", model.GetText()); + + model.ReplaceText(UTF8ToUTF16("-")); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("1234567890-", model.GetText()); + EXPECT_FALSE(model.HasCompositionText()); + EXPECT_FALSE(model.HasSelection()); + + model.SetCompositionText(composition); + model.Append(UTF8ToUTF16("-")); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("1234567890-678-", model.GetText()); + + model.SetCompositionText(composition); + model.Delete(); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("1234567890-678-", model.GetText()); + + model.SetCompositionText(composition); + model.Backspace(); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("1234567890-678-", model.GetText()); + + model.SetText(string16()); + model.SetCompositionText(composition); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, false); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("678", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); + + model.SetCompositionText(composition); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("676788", model.GetText()); + EXPECT_EQ(6U, model.GetCursorPosition()); + + model.SetCompositionText(composition); + model.MoveCursorLeft(gfx::WORD_BREAK, false); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("676788678", model.GetText()); + + model.SetText(string16()); + model.SetCompositionText(composition); + model.MoveCursorRight(gfx::WORD_BREAK, false); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + + model.SetCompositionText(composition); + model.MoveCursorLeft(gfx::LINE_BREAK, true); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("678678", model.GetText()); + + model.SetCompositionText(composition); + model.MoveCursorRight(gfx::LINE_BREAK, false); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("678", model.GetText()); + + model.SetCompositionText(composition); + gfx::SelectionModel sel(model.render_text()->GetSelectionStart(), + 0, 0, gfx::SelectionModel::LEADING); + model.MoveCursorTo(sel); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("678678", model.GetText()); + + model.SetCompositionText(composition); + model.SelectRange(ui::Range(0, 3)); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("678", model.GetText()); + + model.SetCompositionText(composition); + model.SelectAll(); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("678", model.GetText()); + + model.SetCompositionText(composition); + model.SelectWord(); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + EXPECT_STR_EQ("678", model.GetText()); + + model.SetCompositionText(composition); + model.ClearSelection(); + EXPECT_TRUE(composition_text_confirmed_or_cleared_); + composition_text_confirmed_or_cleared_ = false; + + model.SetCompositionText(composition); + EXPECT_FALSE(model.Cut()); + EXPECT_FALSE(composition_text_confirmed_or_cleared_); +} + +TEST_F(TextfieldViewsModelTest, UndoRedo_BasicTest) { + TextfieldViewsModel model(NULL); + model.InsertChar('a'); + EXPECT_FALSE(model.Redo()); // nothing to redo + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("a", model.GetText()); + + // Continuous inserts are treated as one edit. + model.InsertChar('b'); + model.InsertChar('c'); + EXPECT_STR_EQ("abc", model.GetText()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("a", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + + // Undoing further shouldn't change the text. + EXPECT_FALSE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_FALSE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + + // Redoing to the latest text. + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("a", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("abc", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + + // Backspace =============================== + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("ab", model.GetText()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abc", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ab", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); + // Continous backspaces are treated as one edit. + EXPECT_TRUE(model.Backspace()); + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("", model.GetText()); + // Extra backspace shouldn't affect the history. + EXPECT_FALSE(model.Backspace()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ab", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abc", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("a", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + + // Clear history + model.ClearEditHistory(); + EXPECT_FALSE(model.Undo()); + EXPECT_FALSE(model.Redo()); + EXPECT_STR_EQ("a", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + + // Delete =============================== + model.SetText(ASCIIToUTF16("ABCDE")); + model.ClearEditHistory(); + gfx::SelectionModel sel(2); + model.MoveCursorTo(sel); + EXPECT_TRUE(model.Delete()); + EXPECT_STR_EQ("ABDE", model.GetText()); + model.MoveCursorLeft(gfx::LINE_BREAK, false); + EXPECT_TRUE(model.Delete()); + EXPECT_STR_EQ("BDE", model.GetText()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABDE", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABDE", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); + // Continous deletes are treated as one edit. + EXPECT_TRUE(model.Delete()); + EXPECT_TRUE(model.Delete()); + EXPECT_STR_EQ("AB", model.GetText()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABDE", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("AB", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); +} + +TEST_F(TextfieldViewsModelTest, UndoRedo_SetText) { + // This is to test the undo/redo behavior of omnibox. + TextfieldViewsModel model(NULL); + model.InsertChar('w'); + EXPECT_STR_EQ("w", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + model.SetText(ASCIIToUTF16("www.google.com")); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_STR_EQ("www.google.com", model.GetText()); + model.SelectRange(ui::Range(14, 1)); + model.InsertChar('w'); + EXPECT_STR_EQ("ww", model.GetText()); + model.SetText(ASCIIToUTF16("www.google.com")); + model.SelectRange(ui::Range(14, 2)); + model.InsertChar('w'); + EXPECT_STR_EQ("www", model.GetText()); + model.SetText(ASCIIToUTF16("www.google.com")); + model.SelectRange(ui::Range(14, 3)); + model.InsertChar('.'); + EXPECT_STR_EQ("www.", model.GetText()); + model.SetText(ASCIIToUTF16("www.google.com")); + model.SelectRange(ui::Range(14, 4)); + model.InsertChar('y'); + EXPECT_STR_EQ("www.y", model.GetText()); + model.SetText(ASCIIToUTF16("www.youtube.com")); + EXPECT_STR_EQ("www.youtube.com", model.GetText()); + EXPECT_EQ(5U, model.GetCursorPosition()); + + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("www.google.com", model.GetText()); + EXPECT_EQ(4U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("www.google.com", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("www.google.com", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("www.google.com", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + EXPECT_FALSE(model.Undo()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("www.google.com", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("www.google.com", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("www.google.com", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("www.google.com", model.GetText()); + EXPECT_EQ(4U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("www.youtube.com", model.GetText()); + EXPECT_EQ(5U, model.GetCursorPosition()); + EXPECT_FALSE(model.Redo()); +} + +#if defined(USE_AURA) && defined(OS_LINUX) +// This can be re-enabled when aura on linux has clipboard support. +// http://crbug.com/97845 +#define MAYBE_UndoRedo_CutCopyPasteTest DISABLED_UndoRedo_CutCopyPasteTest +#else +#define MAYBE_UndoRedo_CutCopyPasteTest UndoRedo_CutCopyPasteTest +#endif +TEST_F(TextfieldViewsModelTest, MAYBE_UndoRedo_CutCopyPasteTest) { + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("ABCDE")); + EXPECT_FALSE(model.Redo()); // nothing to redo + // Cut + model.SelectRange(ui::Range(1, 3)); + model.Cut(); + EXPECT_STR_EQ("ADE", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + EXPECT_FALSE(model.Undo()); // no more undo + EXPECT_STR_EQ("", model.GetText()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ADE", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_FALSE(model.Redo()); // no more redo + EXPECT_STR_EQ("ADE", model.GetText()); + + model.Paste(); + model.Paste(); + model.Paste(); + EXPECT_STR_EQ("ABCBCBCDE", model.GetText()); + EXPECT_EQ(7U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCBCDE", model.GetText()); + EXPECT_EQ(5U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ADE", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + EXPECT_FALSE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); // Redoing SetText + EXPECT_EQ(0U, model.GetCursorPosition()); + + // Redo + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ADE", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCBCDE", model.GetText()); + EXPECT_EQ(5U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCBCBCDE", model.GetText()); + EXPECT_EQ(7U, model.GetCursorPosition()); + EXPECT_FALSE(model.Redo()); + + // with SelectRange + model.SelectRange(ui::Range(1, 3)); + EXPECT_TRUE(model.Cut()); + EXPECT_STR_EQ("ABCBCDE", model.GetText()); + EXPECT_EQ(1U, model.GetCursorPosition()); + model.SelectRange(ui::Range(1, 1)); + EXPECT_FALSE(model.Cut()); + model.MoveCursorRight(gfx::LINE_BREAK, false); + EXPECT_TRUE(model.Paste()); + EXPECT_STR_EQ("ABCBCDEBC", model.GetText()); + EXPECT_EQ(9U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCBCDE", model.GetText()); + EXPECT_EQ(7U, model.GetCursorPosition()); + // empty cut shouldn't create an edit. + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCBCBCDE", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + + // Copy + ResetModel(&model); + model.SetText(ASCIIToUTF16("12345")); + EXPECT_STR_EQ("12345", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + model.SelectRange(ui::Range(1, 3)); + model.Copy(); // Copy "23" + EXPECT_STR_EQ("12345", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + model.Paste(); // Paste "23" into "23". + EXPECT_STR_EQ("12345", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + model.Paste(); + EXPECT_STR_EQ("1232345", model.GetText()); + EXPECT_EQ(5U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("12345", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + // TODO(oshima): We need to change the return type from bool to enum. + EXPECT_FALSE(model.Undo()); // No text change. + EXPECT_STR_EQ("12345", model.GetText()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_FALSE(model.Undo()); + // Redo + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("12345", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("12345", model.GetText()); // For 1st paste + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("1232345", model.GetText()); + EXPECT_EQ(5U, model.GetCursorPosition()); + EXPECT_FALSE(model.Redo()); + EXPECT_STR_EQ("1232345", model.GetText()); + + // Test using SelectRange + model.SelectRange(ui::Range(1, 3)); + model.Copy(); + EXPECT_STR_EQ("1232345", model.GetText()); + model.MoveCursorRight(gfx::LINE_BREAK, false); + EXPECT_TRUE(model.Paste()); + EXPECT_STR_EQ("123234523", model.GetText()); + EXPECT_EQ(9U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("1232345", model.GetText()); + EXPECT_EQ(7U, model.GetCursorPosition()); +} + +TEST_F(TextfieldViewsModelTest, UndoRedo_CursorTest) { + TextfieldViewsModel model(NULL); + model.InsertChar('a'); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, false); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + model.InsertChar('b'); + // Moving the cursor shouldn't create a new edit. + EXPECT_STR_EQ("ab", model.GetText()); + EXPECT_FALSE(model.Redo()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_FALSE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ab", model.GetText()); + EXPECT_EQ(2U, model.GetCursorPosition()); + EXPECT_FALSE(model.Redo()); +} + +void RunInsertReplaceTest(TextfieldViewsModel& model) { + gfx::SelectionModel sel; + model.GetSelectionModel(&sel); + bool reverse = (sel.selection_start() > sel.selection_end()); + + model.InsertChar('1'); + model.InsertChar('2'); + model.InsertChar('3'); + EXPECT_STR_EQ("a123d", model.GetText()); + EXPECT_EQ(4U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abcd", model.GetText()); + EXPECT_EQ(reverse ? 1U : 3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + EXPECT_FALSE(model.Undo()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("abcd", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); // By SetText + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("a123d", model.GetText()); + EXPECT_EQ(4U, model.GetCursorPosition()); + EXPECT_FALSE(model.Redo()); +} + +void RunOverwriteReplaceTest(TextfieldViewsModel& model) { + gfx::SelectionModel sel; + model.GetSelectionModel(&sel); + bool reverse = (sel.selection_start() > sel.selection_end()); + + model.ReplaceChar('1'); + model.ReplaceChar('2'); + model.ReplaceChar('3'); + model.ReplaceChar('4'); + EXPECT_STR_EQ("a1234", model.GetText()); + EXPECT_EQ(5U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("abcd", model.GetText()); + EXPECT_EQ(reverse ? 1U : 3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + EXPECT_FALSE(model.Undo()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("abcd", model.GetText()); + EXPECT_EQ(0U, model.GetCursorPosition()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("a1234", model.GetText()); + EXPECT_EQ(5U, model.GetCursorPosition()); + EXPECT_FALSE(model.Redo()); +} + +TEST_F(TextfieldViewsModelTest, UndoRedo_ReplaceTest) { + // By Cursor + { + SCOPED_TRACE("forward & insert by cursor"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(1, 3)); + RunInsertReplaceTest(model); + } + { + SCOPED_TRACE("backward & insert by cursor"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(3, 1)); + RunInsertReplaceTest(model); + } + { + SCOPED_TRACE("forward & overwrite by cursor"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(1, 3)); + RunOverwriteReplaceTest(model); + } + { + SCOPED_TRACE("backward & overwrite by cursor"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(3, 1)); + RunOverwriteReplaceTest(model); + } + // By SelectRange API + { + SCOPED_TRACE("forward & insert by SelectRange"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(1, 3)); + RunInsertReplaceTest(model); + } + { + SCOPED_TRACE("backward & insert by SelectRange"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(3, 1)); + RunInsertReplaceTest(model); + } + { + SCOPED_TRACE("forward & overwrite by SelectRange"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(1, 3)); + RunOverwriteReplaceTest(model); + } + { + SCOPED_TRACE("backward & overwrite by SelectRange"); + TextfieldViewsModel model(NULL); + model.SetText(ASCIIToUTF16("abcd")); + model.SelectRange(ui::Range(3, 1)); + RunOverwriteReplaceTest(model); + } +} + +TEST_F(TextfieldViewsModelTest, UndoRedo_CompositionText) { + TextfieldViewsModel model(NULL); + + ui::CompositionText composition; + composition.text = ASCIIToUTF16("abc"); + composition.underlines.push_back(ui::CompositionUnderline(0, 3, 0, false)); + composition.selection = ui::Range(2, 3); + + model.SetText(ASCIIToUTF16("ABCDE")); + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.InsertChar('x'); + EXPECT_STR_EQ("ABCDEx", model.GetText()); + EXPECT_TRUE(model.Undo()); // set composition should forget undone edit. + model.SetCompositionText(composition); + EXPECT_TRUE(model.HasCompositionText()); + EXPECT_TRUE(model.HasSelection()); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + + // Accepting composition + model.ConfirmCompositionText(); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("", model.GetText()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + EXPECT_FALSE(model.Redo()); + + // Canceling composition + model.MoveCursorLeft(gfx::LINE_BREAK, false); + model.SetCompositionText(composition); + EXPECT_STR_EQ("abcABCDEabc", model.GetText()); + model.CancelCompositionText(); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + EXPECT_FALSE(model.Redo()); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + EXPECT_FALSE(model.Redo()); + + // SetText with the same text as the result. + ResetModel(&model); + model.SetText(ASCIIToUTF16("ABCDE")); + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.SetCompositionText(composition); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + model.SetText(ASCIIToUTF16("ABCDEabc")); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + EXPECT_FALSE(model.Redo()); + + // SetText with the different text than the result should not + // remember composition text. + ResetModel(&model); + model.SetText(ASCIIToUTF16("ABCDE")); + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.SetCompositionText(composition); + EXPECT_STR_EQ("ABCDEabc", model.GetText()); + model.SetText(ASCIIToUTF16("1234")); + EXPECT_STR_EQ("1234", model.GetText()); + EXPECT_TRUE(model.Undo()); + EXPECT_STR_EQ("ABCDE", model.GetText()); + EXPECT_TRUE(model.Redo()); + EXPECT_STR_EQ("1234", model.GetText()); + EXPECT_FALSE(model.Redo()); + + // TODO(oshima): We need MockInputMethod to test the behavior with IME. +} + +} // namespace views diff --git a/ui/views/examples/textfield_example.cc b/ui/views/examples/textfield_example.cc index d4ad38e..3a9a7d6 100644 --- a/ui/views/examples/textfield_example.cc +++ b/ui/views/examples/textfield_example.cc @@ -7,9 +7,9 @@ #include "base/utf_string_conversions.h" #include "ui/base/range/range.h" #include "ui/gfx/render_text.h" +#include "ui/views/controls/textfield/textfield.h" #include "ui/views/layout/grid_layout.h" #include "views/controls/label.h" -#include "views/controls/textfield/textfield.h" #include "views/view.h" namespace examples { diff --git a/ui/views/examples/textfield_example.h b/ui/views/examples/textfield_example.h index f3e1d74..36d43cd 100644 --- a/ui/views/examples/textfield_example.h +++ b/ui/views/examples/textfield_example.h @@ -11,9 +11,9 @@ #include "base/basictypes.h" #include "base/compiler_specific.h" #include "base/string16.h" +#include "ui/views/controls/textfield/textfield_controller.h" #include "ui/views/examples/example_base.h" #include "views/controls/button/text_button.h" -#include "views/controls/textfield/textfield_controller.h" namespace examples { diff --git a/ui/views/focus/focus_manager_unittest.cc b/ui/views/focus/focus_manager_unittest.cc index af4ea62..e17d8f1 100644 --- a/ui/views/focus/focus_manager_unittest.cc +++ b/ui/views/focus/focus_manager_unittest.cc @@ -5,12 +5,12 @@ #include "base/utf_string_conversions.h" #include "ui/base/accelerators/accelerator.h" #include "ui/base/keycodes/keyboard_codes.h" +#include "ui/views/controls/textfield/textfield.h" #include "ui/views/focus/accelerator_handler.h" #include "ui/views/focus/focus_manager_factory.h" #include "ui/views/focus/focus_manager_test.h" #include "ui/views/widget/widget.h" #include "views/controls/button/text_button.h" -#include "views/controls/textfield/textfield.h" #if !defined(USE_AURA) #include "ui/views/controls/tabbed_pane/native_tabbed_pane_wrapper.h" diff --git a/ui/views/focus/focus_traversal_unittest.cc b/ui/views/focus/focus_traversal_unittest.cc index 5583291..02151d7 100644 --- a/ui/views/focus/focus_traversal_unittest.cc +++ b/ui/views/focus/focus_traversal_unittest.cc @@ -9,6 +9,7 @@ #include "ui/base/models/combobox_model.h" #include "ui/views/controls/combobox/combobox.h" #include "ui/views/controls/native/native_view_host.h" +#include "ui/views/controls/textfield/textfield.h" #include "ui/views/focus/focus_manager_test.h" #include "ui/views/widget/root_view.h" #include "ui/views/widget/widget.h" @@ -18,7 +19,6 @@ #include "views/controls/label.h" #include "views/controls/link.h" #include "views/controls/scroll_view.h" -#include "views/controls/textfield/textfield.h" #if !defined(USE_AURA) #include "ui/views/controls/tabbed_pane/tabbed_pane.h" diff --git a/ui/views/touchui/touch_selection_controller_impl_unittest.cc b/ui/views/touchui/touch_selection_controller_impl_unittest.cc index 64c9ab1..23423b8 100644 --- a/ui/views/touchui/touch_selection_controller_impl_unittest.cc +++ b/ui/views/touchui/touch_selection_controller_impl_unittest.cc @@ -6,12 +6,12 @@ #include "ui/gfx/point.h" #include "ui/gfx/rect.h" #include "ui/gfx/render_text.h" +#include "ui/views/controls/textfield/native_textfield_views.h" +#include "ui/views/controls/textfield/textfield.h" #include "ui/views/test/views_test_base.h" #include "ui/views/touchui/touch_selection_controller.h" #include "ui/views/touchui/touch_selection_controller_impl.h" #include "ui/views/widget/widget.h" -#include "views/controls/textfield/native_textfield_views.h" -#include "views/controls/textfield/textfield.h" namespace views { diff --git a/ui/views/widget/native_widget_gtk.cc b/ui/views/widget/native_widget_gtk.cc index c744216..6192516 100644 --- a/ui/views/widget/native_widget_gtk.cc +++ b/ui/views/widget/native_widget_gtk.cc @@ -34,16 +34,16 @@ #include "ui/gfx/path.h" #include "ui/gfx/screen.h" #include "ui/views/bubble/bubble_delegate.h" +#include "ui/views/controls/textfield/native_textfield_views.h" #include "ui/views/focus/view_storage.h" #include "ui/views/ime/input_method_gtk.h" -#include "views/controls/textfield/native_textfield_views.h" -#include "views/views_delegate.h" #include "ui/views/widget/drop_target_gtk.h" #include "ui/views/widget/gtk_views_fixed.h" #include "ui/views/widget/gtk_views_window.h" #include "ui/views/widget/root_view.h" #include "ui/views/widget/tooltip_manager_gtk.h" #include "ui/views/widget/widget_delegate.h" +#include "views/views_delegate.h" #if defined(HAVE_IBUS) #include "ui/views/ime/input_method_ibus.h" diff --git a/ui/views/widget/native_widget_win.cc b/ui/views/widget/native_widget_win.cc index d49b5fe..8c07a86 100644 --- a/ui/views/widget/native_widget_win.cc +++ b/ui/views/widget/native_widget_win.cc @@ -33,6 +33,7 @@ #include "ui/gfx/path.h" #include "ui/gfx/screen.h" #include "ui/views/accessibility/native_view_accessibility_win.h" +#include "ui/views/controls/textfield/native_textfield_views.h" #include "ui/views/focus/accelerator_handler.h" #include "ui/views/focus/view_storage.h" #include "ui/views/ime/input_method_win.h" @@ -45,7 +46,6 @@ #include "ui/views/widget/widget_delegate.h" #include "ui/views/window/native_frame_view.h" #include "views/controls/native_control_win.h" -#include "views/controls/textfield/native_textfield_views.h" #include "views/views_delegate.h" #pragma comment(lib, "dwmapi.lib") |