diff options
author | oshima@chromium.org <oshima@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-12-17 07:48:55 +0000 |
---|---|---|
committer | oshima@chromium.org <oshima@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-12-17 07:48:55 +0000 |
commit | d98e7ceee03e22fbf3f93a9b4488128843ae59a1 (patch) | |
tree | b07941d88f64a8a56783c55ba59bb18c6806a864 /views | |
parent | 0c81ba665bab0e37ff9b82fdfd71d136044d5c96 (diff) | |
download | chromium_src-d98e7ceee03e22fbf3f93a9b4488128843ae59a1.zip chromium_src-d98e7ceee03e22fbf3f93a9b4488128843ae59a1.tar.gz chromium_src-d98e7ceee03e22fbf3f93a9b4488128843ae59a1.tar.bz2 |
no native implementation of Textfield.
This is based on the original CL http://codereview.chromium.org/3142008.
The key difference is
* This uses Textfield framework and NativeTextfieldView implements NativeTextfieldWrapper.
This allows us to swap the implementation without recompling the tree and can start
testing on bots.
* Changed the name of the model to TextfieldViewModel as TextfieldModel may be confusing
as other Textfield implementations are not using it. I also changed to use string16 instead
of gap buffer as it's enough for single line text. We can update the model to use GapBuffer when necessary.
* Changed to use string16 as that's what chrome codebase should use.
* Added a switch to turn on TextfieldView.
I also filled a couple of features such as:
* selection by key
* mouse actions (move cursor, selection)
* used WordIterator, which is i18n compatible, to move cursor by word
* blinking cursor
This is only for linux based build due to KeyStroke difference.
I'm going to move some of test utlity function in chrome/browser/automation/ui_controls to app/test
and will add more test once the migration is done.
BUG=none
TEST=new unit tests are added : NativeTestfieldViewTest and TextfieldViewModelTest.
Review URL: http://codereview.chromium.org/5857002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@69523 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'views')
-rw-r--r-- | views/controls/textfield/native_textfield_gtk.cc | 15 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_views.cc | 698 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_views.h | 172 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_views_unittest.cc | 226 | ||||
-rw-r--r-- | views/controls/textfield/textfield.cc | 9 | ||||
-rw-r--r-- | views/controls/textfield/textfield.h | 14 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model.cc | 247 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model.h | 158 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model_unittest.cc | 285 | ||||
-rw-r--r-- | views/event.h | 9 | ||||
-rw-r--r-- | views/event_gtk.cc | 8 | ||||
-rw-r--r-- | views/event_x.cc | 3 | ||||
-rw-r--r-- | views/views.gyp | 14 | ||||
-rw-r--r-- | views/widget/root_view.cc | 11 |
14 files changed, 1838 insertions, 31 deletions
diff --git a/views/controls/textfield/native_textfield_gtk.cc b/views/controls/textfield/native_textfield_gtk.cc index e1c000c..2878b3c 100644 --- a/views/controls/textfield/native_textfield_gtk.cc +++ b/views/controls/textfield/native_textfield_gtk.cc @@ -356,7 +356,8 @@ gboolean NativeTextfieldGtk::OnKeyPressEventHandler( gboolean NativeTextfieldGtk::OnKeyPressEvent(GdkEventKey* event) { Textfield::Controller* controller = textfield_->GetController(); if (controller) { - Textfield::Keystroke ks(event); + KeyEvent key_event(event); + Textfield::Keystroke ks(&key_event); return controller->HandleKeystroke(textfield_, ks); } return false; @@ -379,7 +380,8 @@ gboolean NativeTextfieldGtk::OnActivate() { Textfield::Controller* controller = textfield_->GetController(); if (controller) { - Textfield::Keystroke ks(key_event); + KeyEvent views_key_event(key_event); + Textfield::Keystroke ks(&views_key_event); handled = controller->HandleKeystroke(textfield_, ks); } @@ -444,13 +446,4 @@ void NativeTextfieldGtk::NativeControlCreated(GtkWidget* widget) { g_signal_connect(widget, "activate", G_CALLBACK(OnActivateHandler), this); } -//////////////////////////////////////////////////////////////////////////////// -// NativeTextfieldWrapper, public: - -// static -NativeTextfieldWrapper* NativeTextfieldWrapper::CreateWrapper( - Textfield* field) { - return new NativeTextfieldGtk(field); -} - } // namespace views diff --git a/views/controls/textfield/native_textfield_views.cc b/views/controls/textfield/native_textfield_views.cc new file mode 100644 index 0000000..b7c01f8 --- /dev/null +++ b/views/controls/textfield/native_textfield_views.cc @@ -0,0 +1,698 @@ +// Copyright (c) 2010 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 "views/controls/textfield/native_textfield_views.h" + +#include <algorithm> + +#include "base/command_line.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "base/utf_string_conversions.h" +#include "gfx/canvas.h" +#include "gfx/canvas_skia.h" +#include "gfx/insets.h" +#include "views/background.h" +#include "views/border.h" +#include "views/controls/textfield/native_textfield_gtk.h" +#include "views/controls/textfield/textfield.h" +#include "views/controls/textfield/textfield_views_model.h" +#include "views/event.h" + +namespace { + +// A global flag to switch the Textfield wrapper to TextfieldViews. +bool textfield_view_enabled = false; + +// Color setttings for text, border, backgrounds and cursor. +// These are tentative, and should be derived from theme, system +// settings and current settings. +const SkColor kSelectedTextColor = SK_ColorWHITE; +const SkColor kReadonlyTextColor = SK_ColorDKGRAY; +const SkColor kFocusedSelectionColor = SK_ColorBLUE; +const SkColor kUnfocusedSelectionColor = SK_ColorLTGRAY; +const SkColor kFocusedBorderColor = SK_ColorCYAN; +const SkColor kDefaultBorderColor = SK_ColorGRAY; +const SkColor kCursorColor = SK_ColorBLACK; + +// Parameters to control cursor blinking. +const int kCursorVisibleTimeMs = 800; +const int kCursorInvisibleTimeMs = 500; + +// A switch to enable NativeTextfieldViews; +const char kEnableViewsBasedTextfieldSwitch[] = "enable-textfield-view"; +} // namespace + +namespace views { + +const char NativeTextfieldViews::kViewClassName[] = + "views/NativeTextfieldViews"; + +NativeTextfieldViews::NativeTextfieldViews(Textfield* parent) + : textfield_(parent), + model_(new TextfieldViewsModel()), + text_border_(new TextfieldBorder()), + text_offset_(0), + insert_(true), + is_cursor_visible_(false), + ALLOW_THIS_IN_INITIALIZER_LIST(cursor_timer_(this)) { + SetFocusable(true); + set_border(text_border_); + + // Multiline is not supported. + DCHECK_NE(parent->style(), Textfield::STYLE_MULTILINE); + // Lowercase is not supported. + DCHECK_NE(parent->style(), Textfield::STYLE_LOWERCASE); +} + +NativeTextfieldViews::~NativeTextfieldViews() { +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldViews, View overrides: + +bool NativeTextfieldViews::OnMousePressed(const views::MouseEvent& e) { + RequestFocus(); + size_t pos = FindCursorPosition(e.location()); + if (model_->MoveCursorTo(pos, false)) { + UpdateCursorBoundsAndTextOffset(); + SchedulePaint(); + } + return true; +} + +bool NativeTextfieldViews::OnMouseDragged(const views::MouseEvent& e) { + size_t pos = FindCursorPosition(e.location()); + if (model_->MoveCursorTo(pos, true)) { + UpdateCursorBoundsAndTextOffset(); + SchedulePaint(); + } + return true; +} + +void NativeTextfieldViews::OnMouseReleased(const views::MouseEvent& e, + bool canceled) { +} + +bool NativeTextfieldViews::OnKeyPressed(const views::KeyEvent& e) { + Textfield::Controller* controller = textfield_->GetController(); + bool handled = false; + if (controller) { + Textfield::Keystroke ks(&e); + handled = controller->HandleKeystroke(textfield_, ks); + } + return handled || HandleKeyEvent(e); +} + +bool NativeTextfieldViews::OnKeyReleased(const views::KeyEvent& e) { + return true; +} + +void NativeTextfieldViews::Paint(gfx::Canvas* canvas) { + text_border_->set_has_focus(HasFocus()); + PaintBackground(canvas); + PaintTextAndCursor(canvas); + if (textfield_->draw_border()) + PaintBorder(canvas); +} + +void NativeTextfieldViews::WillGainFocus() { +} + +void NativeTextfieldViews::DidGainFocus() { + is_cursor_visible_ = true; + SchedulePaint(); + // Start blinking cursor. + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + cursor_timer_.NewRunnableMethod(&NativeTextfieldViews::UpdateCursor), + kCursorVisibleTimeMs); +} + +void NativeTextfieldViews::WillLoseFocus() { + // Stop blinking cursor. + cursor_timer_.RevokeAll(); + if (is_cursor_visible_) { + is_cursor_visible_ = false; + RepaintCursor(); + } +} + +void NativeTextfieldViews::DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + UpdateCursorBoundsAndTextOffset(); +} + +///////////////////////////////////////////////////////////////// +// NativeTextfieldViews, NativeTextifieldWrapper overrides: + +string16 NativeTextfieldViews::GetText() const { + return model_->text(); +} + +void NativeTextfieldViews::UpdateText() { + bool changed = model_->SetText(textfield_->text()); + UpdateCursorBoundsAndTextOffset(); + SchedulePaint(); + if (changed) { + Textfield::Controller* controller = textfield_->GetController(); + if (controller) + controller->ContentsChanged(textfield_, GetText()); + } +} + +void NativeTextfieldViews::AppendText(const string16& text) { + if (text.empty()) + return; + model_->Append(text); + UpdateCursorBoundsAndTextOffset(); + SchedulePaint(); + + Textfield::Controller* controller = textfield_->GetController(); + if (controller) + controller->ContentsChanged(textfield_, GetText()); +} + +string16 NativeTextfieldViews::GetSelectedText() const { + return model_->GetSelectedText(); +} + +void NativeTextfieldViews::SelectAll() { + model_->SelectAll(); + SchedulePaint(); +} + +void NativeTextfieldViews::ClearSelection() { + model_->ClearSelection(); + SchedulePaint(); +} + +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() { + SchedulePaint(); +} + +void NativeTextfieldViews::UpdateFont() { + UpdateCursorBoundsAndTextOffset(); +} + +void NativeTextfieldViews::UpdateIsPassword() { + model_->set_is_password(textfield_->IsPassword()); + UpdateCursorBoundsAndTextOffset(); + SchedulePaint(); +} + +void NativeTextfieldViews::UpdateEnabled() { + SchedulePaint(); +} + +bool NativeTextfieldViews::IsPassword() { + // looks unnecessary. should we remove? + NOTREACHED(); + return false; +} + +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); + UpdateCursorBoundsAndTextOffset(); +} + +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()); + UpdateCursorBoundsAndTextOffset(); +} + +void NativeTextfieldViews::SetFocus() { + RequestFocus(); +} + +View* NativeTextfieldViews::GetView() { + return this; +} + +gfx::NativeView NativeTextfieldViews::GetTestingHandle() const { + NOTREACHED(); + return NULL; +} + +bool NativeTextfieldViews::IsIMEComposing() const { + return false; +} + +// static +bool NativeTextfieldViews::IsTextfieldViewsEnabled() { +#if defined(TOUCH_UI) + return true; +#else + return textfield_view_enabled || + CommandLine::ForCurrentProcess()->HasSwitch( + kEnableViewsBasedTextfieldSwitch); +#endif +} + +// static +void NativeTextfieldViews::SetEnableTextfieldViews(bool enabled) { + textfield_view_enabled = enabled; +} + + +/////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldViews private: + +const gfx::Font& NativeTextfieldViews::GetFont() const { + return textfield_->font(); +} + +SkColor NativeTextfieldViews::GetTextColor() const { + return textfield_->text_color(); +} + +void NativeTextfieldViews::UpdateCursor() { + is_cursor_visible_ = !is_cursor_visible_; + RepaintCursor(); + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + cursor_timer_.NewRunnableMethod(&NativeTextfieldViews::UpdateCursor), + is_cursor_visible_ ? kCursorVisibleTimeMs : kCursorInvisibleTimeMs); +} + +void NativeTextfieldViews::RepaintCursor() { + gfx::Rect r = cursor_bounds_; + r.Inset(-1, -1, -1, -1); + SchedulePaint(r, false); +} + +void NativeTextfieldViews::UpdateCursorBoundsAndTextOffset() { + if (bounds().IsEmpty()) + return; + + gfx::Insets insets = GetInsets(); + + int width = bounds().width() - insets.width(); + + // TODO(oshima): bidi + const gfx::Font& font = GetFont(); + int full_width = font.GetStringWidth(UTF16ToWide(model_->GetVisibleText())); + cursor_bounds_ = model_->GetCursorBounds(font); + cursor_bounds_.set_y(cursor_bounds_.y() + insets.top()); + + int x_right = text_offset_ + cursor_bounds_.right(); + int x_left = text_offset_ + cursor_bounds_.x(); + + if (full_width < width) { + // Show all text whenever the text fits to the size. + text_offset_ = 0; + } else if (x_right > width) { + // when the cursor overflows to the right + text_offset_ = width - cursor_bounds_.right(); + } else if (x_left < 0) { + // when the cursor overflows to the left + text_offset_ = -cursor_bounds_.x(); + } else if(full_width > width && text_offset_ + full_width < width) { + // when the cursor moves within the textfield with the text + // longer than the field. + text_offset_ = width - full_width; + } else { + // move cursor freely. + } + // shift cursor bounds to fit insets. + cursor_bounds_.set_x(cursor_bounds_.x() + text_offset_ + insets.left()); +} + +void NativeTextfieldViews::PaintTextAndCursor(gfx::Canvas* canvas) { + gfx::Insets insets = GetInsets(); + + canvas->Save(); + canvas->ClipRectInt(insets.left(), insets.top(), + width() - insets.width(), height() - insets.height()); + + // TODO(oshima): bidi support + // TODO(varunjain): re-implement this so only that dirty text is painted. + TextfieldViewsModel::TextFragments fragments; + model_->GetFragments(&fragments); + int x_offset = text_offset_ + insets.left(); + int y = insets.top(); + int text_height = height() - insets.height(); + SkColor selection_color = + HasFocus() ? kFocusedSelectionColor : kUnfocusedSelectionColor; + SkColor text_color = + textfield_->read_only() ? kReadonlyTextColor : GetTextColor(); + + for (TextfieldViewsModel::TextFragments::const_iterator iter = + fragments.begin(); + iter != fragments.end(); + iter++) { + string16 text = model_->GetVisibleText((*iter).begin, (*iter).end); + // TODO(oshima): This does not give the accurate position due to + // kerning. Figure out how webkit does this with skia. + int width = GetFont().GetStringWidth(UTF16ToWide(text)); + + if ((*iter).selected) { + canvas->FillRectInt(selection_color, x_offset, y, width, text_height); + canvas->DrawStringInt( + UTF16ToWide(text), GetFont(), kSelectedTextColor, + x_offset, y, width, text_height); + } else { + canvas->DrawStringInt( + UTF16ToWide(text), GetFont(), text_color, + x_offset, y, width, text_height); + } + x_offset += width; + } + canvas->Restore(); + + if (textfield_->IsEnabled() && is_cursor_visible_ && + !model_->HasSelection()) { + // Paint Cursor. Replace cursor is drawn as rectangle for now. + canvas->DrawRectInt(kCursorColor, + cursor_bounds_.x(), + cursor_bounds_.y(), + insert_ ? 0 : cursor_bounds_.width(), + cursor_bounds_.height()); + } +} + +bool NativeTextfieldViews::HandleKeyEvent(const KeyEvent& key_event) { + // TODO(oshima): handle IME. + if (key_event.GetType() == views::Event::ET_KEY_PRESSED) { + app::KeyboardCode key_code = key_event.GetKeyCode(); + // TODO(oshima): shift-tab does not work. Figure out why and fix. + if (key_code == app::VKEY_TAB) + return false; + bool selection = key_event.IsShiftDown(); + bool control = key_event.IsControlDown(); + bool text_changed = false; + bool cursor_changed = false; + switch (key_code) { + case app::VKEY_A: + if (control) { + model_->SelectAll(); + cursor_changed = true; + } + break; + case app::VKEY_RIGHT: + control ? model_->MoveCursorToNextWord(selection) + : model_->MoveCursorRight(selection); + cursor_changed = true; + break; + case app::VKEY_LEFT: + control ? model_->MoveCursorToPreviousWord(selection) + : model_->MoveCursorLeft(selection); + cursor_changed = true; + break; + case app::VKEY_END: + model_->MoveCursorToEnd(selection); + cursor_changed = true; + break; + case app::VKEY_HOME: + model_->MoveCursorToStart(selection); + cursor_changed = true; + break; + case app::VKEY_BACK: + text_changed = model_->Backspace(); + cursor_changed = true; + break; + case app::VKEY_DELETE: + text_changed = model_->Delete(); + break; + case app::VKEY_INSERT: + insert_ = !insert_; + cursor_changed = true; + break; + default: + break; + } + char16 print_char = GetPrintableChar(key_event); + if (!control && print_char) { + if (insert_) + model_->Insert(print_char); + else + model_->Replace(print_char); + text_changed = true; + } + if (text_changed) { + textfield_->SyncText(); + Textfield::Controller* controller = textfield_->GetController(); + if (controller) + controller->ContentsChanged(textfield_, GetText()); + } + if (text_changed || cursor_changed) { + UpdateCursorBoundsAndTextOffset(); + SchedulePaint(); + } + } + return false; +} + +char16 NativeTextfieldViews::GetPrintableChar(const KeyEvent& key_event) { + // TODO(oshima): IME, i18n support. + // This only works for UCS-2 characters. + app::KeyboardCode key_code = key_event.GetKeyCode(); + bool shift = key_event.IsShiftDown() ^ key_event.IsCapsLockDown(); + // TODO(oshima): We should have a utility function + // under app to convert a KeyboardCode to a printable character, + // probably in keyboard_code_conversion{.h, _x + switch (key_code) { + case app::VKEY_NUMPAD0: + return '0'; + case app::VKEY_NUMPAD1: + return '1'; + case app::VKEY_NUMPAD2: + return '2'; + case app::VKEY_NUMPAD3: + return '3'; + case app::VKEY_NUMPAD4: + return '4'; + case app::VKEY_NUMPAD5: + return '5'; + case app::VKEY_NUMPAD6: + return '6'; + case app::VKEY_NUMPAD7: + return '7'; + case app::VKEY_NUMPAD8: + return '8'; + case app::VKEY_NUMPAD9: + return '9'; + case app::VKEY_MULTIPLY: + return '*'; + case app::VKEY_ADD: + return '+'; + case app::VKEY_SUBTRACT: + return '-'; + case app::VKEY_DECIMAL: + return '.'; + case app::VKEY_DIVIDE: + return '/'; + case app::VKEY_SPACE: + return ' '; + case app::VKEY_0: + return shift ? ')' : '0'; + case app::VKEY_1: + return shift ? '!' : '1'; + case app::VKEY_2: + return shift ? '@' : '2'; + case app::VKEY_3: + return shift ? '#' : '3'; + case app::VKEY_4: + return shift ? '$' : '4'; + case app::VKEY_5: + return shift ? '%' : '5'; + case app::VKEY_6: + return shift ? '^' : '6'; + case app::VKEY_7: + return shift ? '&' : '7'; + case app::VKEY_8: + return shift ? '*' : '8'; + case app::VKEY_9: + return shift ? '(' : '9'; + + case app::VKEY_A: + case app::VKEY_B: + case app::VKEY_C: + case app::VKEY_D: + case app::VKEY_E: + case app::VKEY_F: + case app::VKEY_G: + case app::VKEY_H: + case app::VKEY_I: + case app::VKEY_J: + case app::VKEY_K: + case app::VKEY_L: + case app::VKEY_M: + case app::VKEY_N: + case app::VKEY_O: + case app::VKEY_P: + case app::VKEY_Q: + case app::VKEY_R: + case app::VKEY_S: + case app::VKEY_T: + case app::VKEY_U: + case app::VKEY_V: + case app::VKEY_W: + case app::VKEY_X: + case app::VKEY_Y: + case app::VKEY_Z: + return (shift ? 'A' : 'a') + (key_code - app::VKEY_A); + case app::VKEY_OEM_1: + return shift ? ':' : ';'; + case app::VKEY_OEM_PLUS: + return shift ? '+' : '='; + case app::VKEY_OEM_COMMA: + return shift ? '<' : ','; + case app::VKEY_OEM_MINUS: + return shift ? '_' : '-'; + case app::VKEY_OEM_PERIOD: + return shift ? '>' : '.'; + case app::VKEY_OEM_2: + return shift ? '?' : '/'; + case app::VKEY_OEM_3: + return shift ? '~' : '`'; + case app::VKEY_OEM_4: + return shift ? '}' : ']'; + case app::VKEY_OEM_5: + return shift ? '|' : '\\'; + case app::VKEY_OEM_6: + return shift ? '{' : '['; + case app::VKEY_OEM_7: + return shift ? '"' : '\''; + default: + return 0; + } +} + +size_t NativeTextfieldViews::FindCursorPosition(const gfx::Point& point) const { + // TODO(oshima): BIDI/i18n support. + gfx::Font font = GetFont(); + gfx::Insets insets = GetInsets(); + std::wstring text = UTF16ToWide(model_->GetVisibleText()); + int left = 0; + int left_pos = 0; + int right = font.GetStringWidth(text); + int right_pos = text.length(); + + int x = point.x() - insets.left() - text_offset_; + if (x <= left) return left_pos; + if (x >= right) return right_pos; + // binary searching the cursor position. + // TODO(oshima): use the center of character instead of edge. + // Binary search may not work for language like arabic. + while (std::abs(static_cast<long>(right_pos - left_pos) > 1)) { + int pivot_pos = left_pos + (right_pos - left_pos) / 2; + int pivot = font.GetStringWidth(text.substr(0, pivot_pos)); + if (pivot < x) { + left = pivot; + left_pos = pivot_pos; + } else if (pivot == x) { + return pivot_pos; + } else { + right = pivot; + right_pos = pivot_pos; + } + } + return left_pos; +} + +/////////////////////////////////////////////////////////////////////////////// +// NativeTextfieldWrapper: + +// static +NativeTextfieldWrapper* NativeTextfieldWrapper::CreateWrapper( + Textfield* field) { + if (NativeTextfieldViews::IsTextfieldViewsEnabled()) { + return new NativeTextfieldViews(field); + } else { + return new NativeTextfieldGtk(field); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// +// TextifieldBorder +// +/////////////////////////////////////////////////////////////////////////////// + +NativeTextfieldViews::TextfieldBorder::TextfieldBorder() + : has_focus_(false), + insets_(4, 4, 4, 4) { +} + +void NativeTextfieldViews::TextfieldBorder::Paint( + const View& view, gfx::Canvas* canvas) const { + SkRect rect; + rect.set(SkIntToScalar(0), SkIntToScalar(0), + SkIntToScalar(view.width()), SkIntToScalar(view.height())); + SkScalar corners[8] = { + // top-left + insets_.left(), + insets_.top(), + // top-right + insets_.right(), + insets_.top(), + // bottom-right + insets_.right(), + insets_.bottom(), + // bottom-left + insets_.left(), + insets_.bottom(), + }; + SkPath path; + path.addRoundRect(rect, corners); + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setFlags(SkPaint::kAntiAlias_Flag); + // TODO(oshima): Copy what WebKit does for focused border. + paint.setColor(has_focus_ ? kFocusedBorderColor : kDefaultBorderColor); + paint.setStrokeWidth(has_focus_ ? 2 : 1); + + canvas->AsCanvasSkia()->drawPath(path, paint); +} + +void NativeTextfieldViews::TextfieldBorder::GetInsets(gfx::Insets* insets) const +{ + *insets = insets_; +} + +void NativeTextfieldViews::TextfieldBorder::SetInsets(int top, + int left, + int bottom, + int right) { + insets_.Set(top, left, bottom, right); +} + +} // namespace views diff --git a/views/controls/textfield/native_textfield_views.h b/views/controls/textfield/native_textfield_views.h new file mode 100644 index 0000000..cfb47be --- /dev/null +++ b/views/controls/textfield/native_textfield_views.h @@ -0,0 +1,172 @@ +// Copyright (c) 2010 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 VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_VIEWS_H_ +#define VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_VIEWS_H_ +#pragma once + +#include "base/string16.h" +#include "base/task.h" +#include "gfx/font.h" +#include "views/border.h" +#include "views/controls/textfield/native_textfield_wrapper.h" +#include "views/view.h" + +namespace gfx { +class Canvas; +} // namespace + +namespace views { + +class KeyEvent; +class TextfieldViewsModel; + +// A views/skia only implementation of NativeTextfieldWrapper. +// No platform specific code is used. +// Following features are not yet supported. +// * BIDI +// * Clipboard (Cut & Paste). +// * Context Menu. +// * IME/i18n support. +// * X selection (only if we want to support). +// * STYLE_MULTILINE, STYLE_LOWERCASE text. (These are not used in +// chromeos, so we may not need them) +// * Double click to select word, and triple click to select all. +class NativeTextfieldViews : public views::View, + public NativeTextfieldWrapper { + public: + explicit NativeTextfieldViews(Textfield* parent); + ~NativeTextfieldViews(); + + // views::View overrides: + virtual bool OnMousePressed(const views::MouseEvent& e); + virtual bool OnMouseDragged(const views::MouseEvent& e); + virtual void OnMouseReleased(const views::MouseEvent& e, bool canceled); + virtual bool OnKeyPressed(const views::KeyEvent& e); + virtual bool OnKeyReleased(const views::KeyEvent& e); + virtual void Paint(gfx::Canvas* canvas); + virtual void WillGainFocus(); + virtual void DidGainFocus(); + virtual void WillLoseFocus(); + virtual void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current); + + // NativeTextfieldWrapper overrides: + virtual string16 GetText() const; + virtual void UpdateText(); + virtual void AppendText(const string16& text); + virtual string16 GetSelectedText() const; + virtual void SelectAll(); + virtual void ClearSelection(); + virtual void UpdateBorder(); + virtual void UpdateTextColor(); + virtual void UpdateBackgroundColor(); + virtual void UpdateReadOnly(); + virtual void UpdateFont(); + virtual void UpdateIsPassword(); + virtual void UpdateEnabled(); + virtual bool IsPassword(); + virtual gfx::Insets CalculateInsets(); + virtual void UpdateHorizontalMargins(); + virtual void UpdateVerticalMargins(); + virtual void SetFocus(); + virtual View* GetView(); + virtual gfx::NativeView GetTestingHandle() const; + virtual bool IsIMEComposing() const; + + // class name of internal + static const char kViewClassName[]; + + // Returns true when + // 1) built with GYP_DEFIENS="touchui=1" + // 2) enabled by SetEnableTextfieldViews(true) + // 3) enabled by the command line flag "--enable-textfield-view". + static bool IsTextfieldViewsEnabled(); + // Enable/Disable TextfieldViews implementation for Textfield. + static void SetEnableTextfieldViews(bool enabled); + + private: + friend class NativeTextfieldViewsTest; + + // A Border class to draw focus border for the text field. + class TextfieldBorder : public Border { + public: + TextfieldBorder(); + + // Border implementation. + virtual void Paint(const View& view, gfx::Canvas* canvas) const; + virtual void GetInsets(gfx::Insets* insets) const; + + // Sets the insets of the border. + void SetInsets(int top, int left, int bottom, int right); + + // Sets the focus state. + void set_has_focus(bool has_focus) { + has_focus_ = has_focus; + } + + private: + bool has_focus_; + gfx::Insets insets_; + + DISALLOW_COPY_AND_ASSIGN(TextfieldBorder); + }; + + // Returns the Textfield's font. + const gfx::Font& GetFont() const; + + // Returns the Textfield's text color. + SkColor GetTextColor() 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(); + + void PaintTextAndCursor(gfx::Canvas* canvas); + + // Handle the keyevent. + bool HandleKeyEvent(const KeyEvent& key_event); + + // Utility function. Gets the character corresponding to a keyevent. + // Returns 0 if the character is not printable. + char16 GetPrintableChar(const KeyEvent& key_event); + + // Find a cusor position for given |point| in this views coordinates. + size_t FindCursorPosition(const gfx::Point& point) const; + + // 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_. + TextfieldBorder* text_border_; + + // The x offset for the text to be drawn, without insets; + int text_offset_; + + // Cursor's bounds in the textfield's coordinates. + gfx::Rect cursor_bounds_; + + // True if the textfield is in insert mode. + bool insert_; + + // The drawing state of cursor. True to draw. + bool is_cursor_visible_; + + // A runnable method factory for callback to update the cursor. + ScopedRunnableMethodFactory<NativeTextfieldViews> cursor_timer_; + + DISALLOW_COPY_AND_ASSIGN(NativeTextfieldViews); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_TEXTFIELD_NATIVE_TEXTFIELD_VIEWS_H_ diff --git a/views/controls/textfield/native_textfield_views_unittest.cc b/views/controls/textfield/native_textfield_views_unittest.cc new file mode 100644 index 0000000..ed69e54 --- /dev/null +++ b/views/controls/textfield/native_textfield_views_unittest.cc @@ -0,0 +1,226 @@ +// Copyright (c) 2010 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 "app/keyboard_codes.h" +#include "base/message_loop.h" +#include "base/utf_string_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "views/controls/textfield/native_textfield_views.h" +#include "views/controls/textfield/textfield.h" +#include "views/controls/textfield/textfield_views_model.h" +#include "views/event.h" +#include "views/widget/widget.h" + +namespace views { + +#define EXPECT_STR_EQ(ascii, utf16) \ + EXPECT_EQ(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 app/test/. +class NativeTextfieldViewsTest : public ::testing::Test, + public Textfield::Controller { + public: + NativeTextfieldViewsTest() + : widget_(NULL), + textfield_(NULL), + textfield_view_(NULL), + model_(NULL), + message_loop_(MessageLoop::TYPE_UI) { + } + + // ::testing::Test overrides. + virtual void SetUp() { + NativeTextfieldViews::SetEnableTextfieldViews(true); + } + + virtual void TearDown() { + NativeTextfieldViews::SetEnableTextfieldViews(false); + if (widget_) + widget_->Close(); + } + + // Textfield::Controller implementation: + virtual void ContentsChanged(Textfield* sender, + const string16& new_contents){ + last_contents_ = new_contents; + } + + virtual bool HandleKeystroke(Textfield* sender, + const Textfield::Keystroke& keystroke) { + // TODO(oshima): figure out how to test the keystroke. + return false; + } + + void InitTextfield(Textfield::StyleFlags style) { + ASSERT_FALSE(textfield_); + textfield_ = new Textfield(style); + textfield_->SetController(this); + widget_ = Widget::CreatePopupWidget( + Widget::NotTransparent, + Widget::AcceptEvents, + Widget::DeleteOnDestroy, + Widget::DontMirrorOriginInRTL); + widget_->Init(NULL, gfx::Rect()); + widget_->SetContentsView(textfield_); + textfield_view_ + = static_cast<NativeTextfieldViews*>(textfield_->native_wrapper()); + DCHECK(textfield_view_); + model_ = textfield_view_->model_.get(); + } + + void SendKeyEventToTextfieldViews(app::KeyboardCode key_code, + bool shift, + bool control) { + int flags = (shift ? KeyEvent::EF_SHIFT_DOWN : 0) | + (control ? KeyEvent::EF_CONTROL_DOWN : 0); + KeyEvent event(KeyEvent::ET_KEY_PRESSED, key_code, flags, 1, 0); + textfield_view_->OnKeyPressed(event); + } + + void SendKeyEventToTextfieldViews(app::KeyboardCode key_code) { + SendKeyEventToTextfieldViews(key_code, false, false); + } + + protected: + // We need widget to populate wrapper class. + Widget* widget_; + + Textfield* textfield_; + NativeTextfieldViews* textfield_view_; + TextfieldViewsModel* model_; + + // A fake message loop for view's drawing events. + MessageLoop message_loop_; + + // The string from Controller::ContentsChanged callback. + string16 last_contents_; + + private: + DISALLOW_COPY_AND_ASSIGN(NativeTextfieldViewsTest); +}; + +TEST_F(NativeTextfieldViewsTest, ModelChangesTeset) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("this is")); + + EXPECT_STR_EQ("this is", model_->text()); + EXPECT_STR_EQ("this is", last_contents_); + last_contents_.clear(); + + textfield_->AppendText(ASCIIToUTF16(" a test")); + EXPECT_STR_EQ("this is a test", model_->text()); + EXPECT_STR_EQ("this is a test", last_contents_); + last_contents_.clear(); + + // Cases where the callback should not be called. + textfield_->SetText(ASCIIToUTF16("this is a test")); + EXPECT_STR_EQ("this is a test", model_->text()); + EXPECT_EQ(string16(), last_contents_); + + textfield_->AppendText(string16()); + EXPECT_STR_EQ("this is a test", model_->text()); + EXPECT_EQ(string16(), last_contents_); + + EXPECT_EQ(string16(), textfield_->GetSelectedText()); + textfield_->SelectAll(); + EXPECT_STR_EQ("this is a test", textfield_->GetSelectedText()); + EXPECT_EQ(string16(), last_contents_); +} + +TEST_F(NativeTextfieldViewsTest, KeyTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + SendKeyEventToTextfieldViews(app::VKEY_C, true, false); + EXPECT_STR_EQ("C", textfield_->text()); + EXPECT_STR_EQ("C", last_contents_); + last_contents_.clear(); + + SendKeyEventToTextfieldViews(app::VKEY_R, false, false); + EXPECT_STR_EQ("Cr", textfield_->text()); + EXPECT_STR_EQ("Cr", last_contents_); +} + +TEST_F(NativeTextfieldViewsTest, ControlAndSelectTest) { + // Insert a test string in a textfield. + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(ASCIIToUTF16("one two three")); + SendKeyEventToTextfieldViews(app::VKEY_RIGHT, + true /* shift */, false /* control */); + SendKeyEventToTextfieldViews(app::VKEY_RIGHT, true, false); + SendKeyEventToTextfieldViews(app::VKEY_RIGHT, true, false); + + EXPECT_STR_EQ("one", textfield_->GetSelectedText()); + + // Test word select. + SendKeyEventToTextfieldViews(app::VKEY_RIGHT, true, true); + EXPECT_STR_EQ("one two", textfield_->GetSelectedText()); + SendKeyEventToTextfieldViews(app::VKEY_RIGHT, true, true); + EXPECT_STR_EQ("one two three", textfield_->GetSelectedText()); + SendKeyEventToTextfieldViews(app::VKEY_LEFT, true, true); + EXPECT_STR_EQ("one two ", textfield_->GetSelectedText()); + SendKeyEventToTextfieldViews(app::VKEY_LEFT, true, true); + EXPECT_STR_EQ("one ", textfield_->GetSelectedText()); + + // Replace the selected text. + SendKeyEventToTextfieldViews(app::VKEY_Z, true, false); + SendKeyEventToTextfieldViews(app::VKEY_E, true, false); + SendKeyEventToTextfieldViews(app::VKEY_R, true, false); + SendKeyEventToTextfieldViews(app::VKEY_O, true, false); + SendKeyEventToTextfieldViews(app::VKEY_SPACE, false, false); + EXPECT_STR_EQ("ZERO two three", textfield_->text()); + + SendKeyEventToTextfieldViews(app::VKEY_END, true, false); + EXPECT_STR_EQ("two three", textfield_->GetSelectedText()); + SendKeyEventToTextfieldViews(app::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]; + app::KeyboardCode code = + c == ' ' ? app::VKEY_SPACE : + static_cast<app::KeyboardCode>(app::VKEY_A + c - 'a'); + SendKeyEventToTextfieldViews(code); + } + EXPECT_STR_EQ(test_str, textfield_->text()); + + // Move the cursor around. + for (int i = 0; i < 6; i++) { + SendKeyEventToTextfieldViews(app::VKEY_LEFT); + } + SendKeyEventToTextfieldViews(app::VKEY_RIGHT); + + // Delete using backspace and check resulting string. + SendKeyEventToTextfieldViews(app::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++) { + SendKeyEventToTextfieldViews(app::VKEY_DELETE); + } + EXPECT_STR_EQ("this is ", textfield_->text()); + + // Select all and replace with "k". + textfield_->SelectAll(); + SendKeyEventToTextfieldViews(app::VKEY_K); + EXPECT_STR_EQ("k", textfield_->text()); +} + +TEST_F(NativeTextfieldViewsTest, PasswordTest) { + InitTextfield(Textfield::STYLE_PASSWORD); + 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_STR_EQ("my password", last_contents_); +} + +} // namespace views diff --git a/views/controls/textfield/textfield.cc b/views/controls/textfield/textfield.cc index 81c74d2..e92794d 100644 --- a/views/controls/textfield/textfield.cc +++ b/views/controls/textfield/textfield.cc @@ -358,8 +358,7 @@ app::KeyboardCode Textfield::Keystroke::GetKeyboardCode() const { #if defined(OS_WIN) return static_cast<app::KeyboardCode>(key_); #else - return static_cast<app::KeyboardCode>( - app::WindowsKeyCodeForGdkKeyCode(event_.keyval)); + return event_->GetKeyCode(); #endif } @@ -373,13 +372,11 @@ bool Textfield::Keystroke::IsShiftHeld() const { } #else bool Textfield::Keystroke::IsControlHeld() const { - return (event_.state & gtk_accelerator_get_default_mod_mask()) == - GDK_CONTROL_MASK; + return event_->IsControlDown(); } bool Textfield::Keystroke::IsShiftHeld() const { - return (event_.state & gtk_accelerator_get_default_mod_mask()) == - GDK_SHIFT_MASK; + return event_->IsShiftDown(); } #endif diff --git a/views/controls/textfield/textfield.h b/views/controls/textfield/textfield.h index d171eb5..9dd1ee9 100644 --- a/views/controls/textfield/textfield.h +++ b/views/controls/textfield/textfield.h @@ -60,10 +60,11 @@ class Textfield : public View { int repeat_count() const { return repeat_count_; } unsigned int flags() const { return flags_; } #else - explicit Keystroke(GdkEventKey* event) - : event_(*event) { + explicit Keystroke(const KeyEvent* event) + : event_(event) { } - const GdkEventKey* event() const { return &event_; } + const KeyEvent& key_event() const { return *event_;}; + const GdkEventKey* event() const { return event_->native_event(); } #endif app::KeyboardCode GetKeyboardCode() const; bool IsControlHeld() const; @@ -76,7 +77,7 @@ class Textfield : public View { int repeat_count_; unsigned int flags_; #else - GdkEventKey event_; + const KeyEvent* event_; #endif DISALLOW_COPY_AND_ASSIGN(Keystroke); @@ -169,7 +170,7 @@ class Textfield : public View { void UseDefaultBackgroundColor(); // Gets/Sets the font used when rendering the text within the Textfield. - gfx::Font font() const { return font_; } + 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 @@ -230,6 +231,9 @@ class Textfield : public View { gfx::NativeView GetTestingHandle() const { return native_wrapper_ ? native_wrapper_->GetTestingHandle() : NULL; } + NativeTextfieldWrapper* native_wrapper() const { + return native_wrapper_; + } #endif // Overridden from View: diff --git a/views/controls/textfield/textfield_views_model.cc b/views/controls/textfield/textfield_views_model.cc new file mode 100644 index 0000000..21783ec --- /dev/null +++ b/views/controls/textfield/textfield_views_model.cc @@ -0,0 +1,247 @@ +// Copyright (c) 2010 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 "views/controls/textfield/textfield_views_model.h" + +#include <algorithm> + +#include "base/i18n/break_iterator.h" +#include "base/logging.h" +#include "base/utf_string_conversions.h" +#include "gfx/font.h" + +namespace views { + +TextfieldViewsModel::TextfieldViewsModel() + : cursor_pos_(0), + selection_begin_(0), + is_password_(false) { +} + +TextfieldViewsModel::~TextfieldViewsModel() { +} + +void TextfieldViewsModel::GetFragments(TextFragments* fragments) const { + DCHECK(fragments); + fragments->clear(); + if (HasSelection()) { + int begin = std::min(selection_begin_, cursor_pos_); + int end = std::max(selection_begin_, cursor_pos_); + if (begin != 0) { + fragments->push_back(TextFragment(0, begin, false)); + } + fragments->push_back(TextFragment(begin, end, true)); + int len = text_.length(); + if (end != len) { + fragments->push_back(TextFragment(end, len, false)); + } + } else { + fragments->push_back(TextFragment(0, text_.length(), false)); + } +} + +bool TextfieldViewsModel::SetText(const string16& text) { + bool changed = text_ != text; + if (changed) { + text_ = text; + if (cursor_pos_ > text.length()) { + cursor_pos_ = text.length(); + } + } + ClearSelection(); + return changed; +} + +void TextfieldViewsModel::Insert(char16 c) { + if (HasSelection()) + DeleteSelection(); + text_.insert(cursor_pos_, 1, c); + cursor_pos_++; + ClearSelection(); +} + +void TextfieldViewsModel::Replace(char16 c) { + if (!HasSelection()) + Delete(); + Insert(c); +} + +void TextfieldViewsModel::Append(const string16& text) { + text_ += text; +} + +bool TextfieldViewsModel::Delete() { + if (HasSelection()) { + DeleteSelection(); + return true; + } else if (text_.length() > cursor_pos_) { + text_.erase(cursor_pos_, 1); + return true; + } + return false; +} + +bool TextfieldViewsModel::Backspace() { + if (HasSelection()) { + DeleteSelection(); + return true; + } else if (cursor_pos_ > 0) { + cursor_pos_--; + text_.erase(cursor_pos_, 1); + ClearSelection(); + return true; + } + return false; +} + +void TextfieldViewsModel::MoveCursorLeft(bool select) { + // TODO(oshima): support BIDI + if (select) { + if (cursor_pos_ > 0) + cursor_pos_--; + } else { + if (HasSelection()) + cursor_pos_ = std::min(cursor_pos_, selection_begin_); + else if (cursor_pos_ > 0) + cursor_pos_--; + ClearSelection(); + } +} + +void TextfieldViewsModel::MoveCursorRight(bool select) { + // TODO(oshima): support BIDI + if (select) { + cursor_pos_ = std::min(text_.length(), cursor_pos_ + 1); + } else { + if (HasSelection()) + cursor_pos_ = std::max(cursor_pos_, selection_begin_); + else + cursor_pos_ = std::min(text_.length(), cursor_pos_ + 1); + ClearSelection(); + } +} + +void TextfieldViewsModel::MoveCursorToPreviousWord(bool select) { + // Notes: We always iterate words from the begining. + // This is probably fast enough for our usage, but we may + // want to modify WordIterator so that it can start from the + // middle of string and advance backwards. + base::BreakIterator iter(&text_, base::BreakIterator::BREAK_WORD); + bool success = iter.Init(); + DCHECK(success); + if (!success) + return; + int prev = 0; + while (iter.Advance()) { + if (iter.IsWord()) { + size_t begin = iter.pos() - iter.GetString().length(); + if (begin == cursor_pos_) { + // The cursor is at the beginning of a word. + // Move to previous word. + cursor_pos_ = prev; + } else if(iter.pos() >= cursor_pos_) { + // The cursor is in the middle or at the end of a word. + // Move to the top of current word. + cursor_pos_ = begin; + } else { + prev = iter.pos() - iter.GetString().length(); + continue; + } + if (!select) + ClearSelection(); + break; + } + } +} + +void TextfieldViewsModel::MoveCursorToNextWord(bool select) { + base::BreakIterator iter(&text_, base::BreakIterator::BREAK_WORD); + bool success = iter.Init(); + DCHECK(success); + if (!success) + return; + while (iter.Advance()) { + if (iter.IsWord() && iter.pos() > cursor_pos_) { + cursor_pos_ = iter.pos(); + if (!select) + ClearSelection(); + break; + } + } +} + +void TextfieldViewsModel::MoveCursorToStart(bool select) { + cursor_pos_ = 0; + if (!select) + ClearSelection(); +} + +void TextfieldViewsModel::MoveCursorToEnd(bool select) { + cursor_pos_ = text_.length(); + if (!select) + ClearSelection(); +} + +bool TextfieldViewsModel::MoveCursorTo(size_t pos, bool select) { + bool cursor_changed = false; + if (cursor_pos_ != pos) { + cursor_pos_ = pos; + cursor_changed = true; + } + if (!select) + ClearSelection(); + return cursor_changed; +} + +gfx::Rect TextfieldViewsModel::GetCursorBounds(const gfx::Font& font) const { + string16 text = GetVisibleText(); + int x = font.GetStringWidth(UTF16ToWide(text.substr(0U, cursor_pos_))); + int h = font.GetHeight(); + DCHECK(x >= 0); + if (text.length() == cursor_pos_) { + return gfx::Rect(x, 0, 0, h); + } else { + int x_end = + font.GetStringWidth(UTF16ToWide(text.substr(0U, cursor_pos_ + 1U))); + return gfx::Rect(x, 0, x_end - x, h); + } +} + +string16 TextfieldViewsModel::GetSelectedText() const { + return text_.substr( + std::min(cursor_pos_, selection_begin_), + std::abs(static_cast<long>(cursor_pos_ - selection_begin_))); +} + +void TextfieldViewsModel::SelectAll() { + cursor_pos_ = 0; + selection_begin_ = text_.length(); +} + +void TextfieldViewsModel::ClearSelection() { + selection_begin_ = cursor_pos_; +} + +bool TextfieldViewsModel::HasSelection() const { + return selection_begin_ != cursor_pos_; +} + +void TextfieldViewsModel::DeleteSelection() { + DCHECK(HasSelection()); + size_t n = std::abs(static_cast<long>(cursor_pos_ - selection_begin_)); + size_t begin = std::min(cursor_pos_, selection_begin_); + text_.erase(begin, n); + cursor_pos_ = begin; + ClearSelection(); +} + +string16 TextfieldViewsModel::GetVisibleText(size_t begin, size_t end) const { + DCHECK(end >= begin); + if (is_password_) { + return string16(end - begin, '*'); + } + return text_.substr(begin, end - begin); +} + +} // namespace views diff --git a/views/controls/textfield/textfield_views_model.h b/views/controls/textfield/textfield_views_model.h new file mode 100644 index 0000000..219a8c7 --- /dev/null +++ b/views/controls/textfield/textfield_views_model.h @@ -0,0 +1,158 @@ +// Copyright (c) 2010 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 VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_VIEWS_MODEL_H_ +#define VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_VIEWS_MODEL_H_ +#pragma once + +#include <vector> + +#include "base/string16.h" +#include "gfx/rect.h" +#include "third_party/skia/include/core/SkColor.h" + +namespace gfx { +class Font; +} // namespace gfx + +namespace views { + +// A model that represents a text content for TextfieldViews. +// It supports editing, selection and cursor manipulation. +class TextfieldViewsModel { + public: + TextfieldViewsModel(); + virtual ~TextfieldViewsModel(); + + // Text fragment info. Used to draw selected text. + // We may replace this with TextAttribute class + // in the future to support multi-color text + // for omnibox. + struct TextFragment { + TextFragment(size_t b, size_t e, bool s) + : begin(b), end(e), selected(s) { + } + // The begin and end position of text fragment. + size_t begin, end; + // True if the text is selected. + bool selected; + }; + typedef std::vector<TextFragment> TextFragments; + + // Gets the text element info. + void GetFragments(TextFragments* elements) const; + + void set_is_password(bool is_password) { + is_password_ = is_password; + } + const string16& text() const { return text_; } + + // Edit related methods. + + // Sest the text. Returns true if the text has been modified. + bool SetText(const string16& text); + + // Inserts a character at the current cursor position. + void Insert(char16 c); + + // Replaces the char at the current position with given character. + void Replace(char16 c); + + // Appends the text. + 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. + 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. + bool Backspace(); + + // Cursor related methods. + + // Returns the current cursor position. + size_t cursor_pos() const { return cursor_pos_; } + + // Moves the cursor left by one position (as if, the user has pressed the left + // arrow key). If |select| is true, it updates the selection accordingly. + void MoveCursorLeft(bool select); + + // Moves the cursor right by one position (as if, the user has pressed the + // right arrow key). If |select| is true, it updates the selection + // accordingly. + void MoveCursorRight(bool select); + + // Moves the cursor left by one word (word boundry is defined by space). + // If |select| is true, it updates the selection accordingly. + void MoveCursorToPreviousWord(bool select); + + // Moves the cursor right by one word (word boundry is defined by space). + // If |select| is true, it updates the selection accordingly. + void MoveCursorToNextWord(bool select); + + // Moves the cursor to start of the textfield contents. + // If |select| is true, it updates the selection accordingly. + void MoveCursorToStart(bool select); + + // Moves the cursor to end of the textfield contents. + // If |select| is true, it updates the selection accordingly. + void MoveCursorToEnd(bool select); + + // Moves the cursor to the specified |position|. + // If |select| is true, it updates the selection accordingly. + bool MoveCursorTo(size_t position, bool select); + + // Returns the bounds of character at the current cursor. + gfx::Rect GetCursorBounds(const gfx::Font& font) const; + + // Selection related method + + // Returns the selected text. + string16 GetSelectedText() const; + + // Selects all text. + void SelectAll(); + + // Clears selection. + void ClearSelection(); + + // Returns visible text. If the field is password, it returns the + // sequence of "*". + string16 GetVisibleText() const { + return GetVisibleText(0U, text_.length()); + } + + private: + friend class NativeTextfieldViews; + + // Tells if any text is selected. + bool HasSelection() const; + + // Deletes the selected text. + void DeleteSelection(); + + // Returns the visible text given |start| and |end|. + string16 GetVisibleText(size_t start, size_t end) const; + + // The text in utf16 format. + string16 text_; + + // Current cursor position. + size_t cursor_pos_; + + // Selection range. + size_t selection_begin_; + + // True if the text is the password. + bool is_password_; + + DISALLOW_COPY_AND_ASSIGN(TextfieldViewsModel); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_VIEWS_MODEL_H_ diff --git a/views/controls/textfield/textfield_views_model_unittest.cc b/views/controls/textfield/textfield_views_model_unittest.cc new file mode 100644 index 0000000..fa09efa --- /dev/null +++ b/views/controls/textfield/textfield_views_model_unittest.cc @@ -0,0 +1,285 @@ +// Copyright (c) 2010 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/utf_string_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "views/controls/textfield/textfield_views_model.h" + +namespace views { + +#define EXPECT_STR_EQ(ascii, utf16) \ + EXPECT_EQ(ASCIIToWide(ascii), UTF16ToWide(utf16)) + +TEST(TextfieldViewsModelTest, EditString) { + TextfieldViewsModel model; + // append two strings + model.Append(ASCIIToUTF16("HILL")); + EXPECT_STR_EQ("HILL", model.text()); + model.Append(ASCIIToUTF16("WORLD")); + EXPECT_STR_EQ("HILLWORLD", model.text()); + + // Insert "E" to make hello + model.MoveCursorRight(false); + model.Insert('E'); + EXPECT_STR_EQ("HEILLWORLD", model.text()); + // Replace "I" with "L" + model.Replace('L'); + EXPECT_STR_EQ("HELLLWORLD", model.text()); + model.Replace('L'); + model.Replace('O'); + EXPECT_STR_EQ("HELLOWORLD", model.text()); + + // Delete 6th char "W", then delete 5th char O" + EXPECT_EQ(5U, model.cursor_pos()); + EXPECT_TRUE(model.Delete()); + EXPECT_STR_EQ("HELLOORLD", model.text()); + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(4U, model.cursor_pos()); + EXPECT_STR_EQ("HELLORLD", model.text()); + + // Move the cursor to start. backspace should fail. + model.MoveCursorToStart(false); + EXPECT_FALSE(model.Backspace()); + EXPECT_STR_EQ("HELLORLD", model.text()); + // Move the cursor to the end. delete should fail. + model.MoveCursorToEnd(false); + EXPECT_FALSE(model.Delete()); + EXPECT_STR_EQ("HELLORLD", model.text()); + // but backspace should work. + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("HELLORL", model.text()); +} + +TEST(TextfieldViewsModelTest, EmptyString) { + TextfieldViewsModel model; + EXPECT_EQ(string16(), model.text()); + EXPECT_EQ(string16(), model.GetSelectedText()); + EXPECT_EQ(string16(), model.GetVisibleText()); + + model.MoveCursorLeft(true); + EXPECT_EQ(0U, model.cursor_pos()); + model.MoveCursorRight(true); + EXPECT_EQ(0U, model.cursor_pos()); + + EXPECT_EQ(string16(), model.GetSelectedText()); + + EXPECT_FALSE(model.Delete()); + EXPECT_FALSE(model.Backspace()); +} + +TEST(TextfieldViewsModelTest, Selection) { + TextfieldViewsModel model; + model.Append(ASCIIToUTF16("HELLO")); + model.MoveCursorRight(false); + model.MoveCursorRight(true); + EXPECT_STR_EQ("E", model.GetSelectedText()); + model.MoveCursorRight(true); + EXPECT_STR_EQ("EL", model.GetSelectedText()); + + model.MoveCursorToStart(true); + EXPECT_STR_EQ("H", model.GetSelectedText()); + model.MoveCursorToEnd(true); + EXPECT_STR_EQ("ELLO", model.GetSelectedText()); + model.ClearSelection(); + EXPECT_EQ(string16(), model.GetSelectedText()); + model.SelectAll(); + EXPECT_STR_EQ("HELLO", model.GetSelectedText()); + + // Select and move cursor + model.MoveCursorTo(1U, false); + model.MoveCursorTo(3U, true); + EXPECT_STR_EQ("EL", model.GetSelectedText()); + model.MoveCursorLeft(false); + EXPECT_EQ(1U, model.cursor_pos()); + model.MoveCursorTo(1U, false); + model.MoveCursorTo(3U, true); + model.MoveCursorRight(false); + EXPECT_EQ(3U, model.cursor_pos()); + + // Select all and move cursor + model.SelectAll(); + model.MoveCursorLeft(false); + EXPECT_EQ(0U, model.cursor_pos()); + model.SelectAll(); + model.MoveCursorRight(false); + EXPECT_EQ(5U, model.cursor_pos()); +} + +TEST(TextfieldViewsModelTest, SelectionAndEdit) { + TextfieldViewsModel model; + model.Append(ASCIIToUTF16("HELLO")); + model.MoveCursorRight(false); + model.MoveCursorRight(true); + model.MoveCursorRight(true); // select "EL" + EXPECT_TRUE(model.Backspace()); + EXPECT_STR_EQ("HLO", model.text()); + + model.Append(ASCIIToUTF16("ILL")); + model.MoveCursorRight(true); + model.MoveCursorRight(true); // select "LO" + EXPECT_TRUE(model.Delete()); + EXPECT_STR_EQ("HILL", model.text()); + EXPECT_EQ(1U, model.cursor_pos()); + model.MoveCursorRight(true); // select "I" + model.Insert('E'); + EXPECT_STR_EQ("HELL", model.text()); + model.MoveCursorToStart(false); + model.MoveCursorRight(true); // select "H" + model.Replace('B'); + EXPECT_STR_EQ("BELL", model.text()); + model.MoveCursorToEnd(false); + model.MoveCursorLeft(true); + model.MoveCursorLeft(true); // select ">LL" + model.Replace('E'); + EXPECT_STR_EQ("BEE", model.text()); +} + +TEST(TextfieldViewsModelTest, Password) { + TextfieldViewsModel model; + model.set_is_password(true); + model.Append(ASCIIToUTF16("HELLO")); + EXPECT_STR_EQ("*****", model.GetVisibleText()); + EXPECT_STR_EQ("HELLO", model.text()); + EXPECT_TRUE(model.Delete()); + + EXPECT_STR_EQ("****", model.GetVisibleText()); + EXPECT_STR_EQ("ELLO", model.text()); + EXPECT_EQ(0U, model.cursor_pos()); + + model.SelectAll(); + EXPECT_STR_EQ("ELLO", model.GetSelectedText()); + EXPECT_EQ(0U, model.cursor_pos()); + + model.Insert('X'); + EXPECT_STR_EQ("*", model.GetVisibleText()); + EXPECT_STR_EQ("X", model.text()); +} + +TEST(TextfieldViewsModelTest, Word) { + TextfieldViewsModel model; + model.Append( + ASCIIToUTF16("The answer to Life, the Universe, and Everything")); + model.MoveCursorToNextWord(false); + EXPECT_EQ(3U, model.cursor_pos()); + model.MoveCursorToNextWord(false); + EXPECT_EQ(10U, model.cursor_pos()); + model.MoveCursorToNextWord(false); + model.MoveCursorToNextWord(false); + EXPECT_EQ(18U, model.cursor_pos()); + + // Should passes the non word char ',' + model.MoveCursorToNextWord(true); + EXPECT_EQ(23U, model.cursor_pos()); + EXPECT_STR_EQ(", the", model.GetSelectedText()); + + // Move to the end. + model.MoveCursorToNextWord(true); + model.MoveCursorToNextWord(true); + model.MoveCursorToNextWord(true); + EXPECT_STR_EQ(", the Universe, and Everything", model.GetSelectedText()); + // Should be safe to go next word at the end. + model.MoveCursorToNextWord(true); + EXPECT_STR_EQ(", the Universe, and Everything", model.GetSelectedText()); + model.Insert('2'); + EXPECT_EQ(19U, model.cursor_pos()); + + // Now backwards. + model.MoveCursorLeft(false); // leave 2. + model.MoveCursorToPreviousWord(true); + EXPECT_EQ(14U, model.cursor_pos()); + EXPECT_STR_EQ("Life", model.GetSelectedText()); + model.MoveCursorToPreviousWord(true); + EXPECT_STR_EQ("to Life", model.GetSelectedText()); + model.MoveCursorToPreviousWord(true); + model.MoveCursorToPreviousWord(true); + model.MoveCursorToPreviousWord(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.MoveCursorToPreviousWord(true); + EXPECT_STR_EQ("The answer to Life", model.GetSelectedText()); + model.Replace('4'); + EXPECT_EQ(string16(), model.GetSelectedText()); + EXPECT_STR_EQ("42", model.GetVisibleText()); +} + +TEST(TextfieldViewsModelTest, TextFragment) { + TextfieldViewsModel model; + TextfieldViewsModel::TextFragments fragments; + // Empty string + model.GetFragments(&fragments); + EXPECT_EQ(1U, fragments.size()); + fragments.clear(); + EXPECT_EQ(0U, fragments[0].begin); + EXPECT_EQ(0U, fragments[0].end); + EXPECT_FALSE(fragments[0].selected); + + // Some string + model.Append(ASCIIToUTF16("Hello world")); + model.GetFragments(&fragments); + EXPECT_EQ(1U, fragments.size()); + EXPECT_EQ(0U, fragments[0].begin); + EXPECT_EQ(11U, fragments[0].end); + EXPECT_FALSE(fragments[0].selected); + + // Select 1st word + model.MoveCursorToNextWord(true); + model.GetFragments(&fragments); + EXPECT_EQ(2U, fragments.size()); + EXPECT_EQ(0U, fragments[0].begin); + EXPECT_EQ(5U, fragments[0].end); + EXPECT_TRUE(fragments[0].selected); + EXPECT_EQ(5U, fragments[1].begin); + EXPECT_EQ(11U, fragments[1].end); + EXPECT_FALSE(fragments[1].selected); + + // Select empty string + model.ClearSelection(); + model.MoveCursorRight(true); + model.GetFragments(&fragments); + EXPECT_EQ(3U, fragments.size()); + EXPECT_EQ(0U, fragments[0].begin); + EXPECT_EQ(5U, fragments[0].end); + EXPECT_FALSE(fragments[0].selected); + EXPECT_EQ(5U, fragments[1].begin); + EXPECT_EQ(6U, fragments[1].end); + EXPECT_TRUE(fragments[1].selected); + + EXPECT_EQ(6U, fragments[2].begin); + EXPECT_EQ(11U, fragments[2].end); + EXPECT_FALSE(fragments[2].selected); + + // Select to the end. + model.MoveCursorToEnd(true); + model.GetFragments(&fragments); + EXPECT_EQ(2U, fragments.size()); + EXPECT_EQ(0U, fragments[0].begin); + EXPECT_EQ(5U, fragments[0].end); + EXPECT_FALSE(fragments[0].selected); + EXPECT_EQ(5U, fragments[1].begin); + EXPECT_EQ(11U, fragments[1].end); + EXPECT_TRUE(fragments[1].selected); +} + +TEST(TextfieldViewsModelTest, SetText) { + TextfieldViewsModel model; + model.Append(ASCIIToUTF16("HELLO")); + model.MoveCursorToEnd(false); + model.SetText(ASCIIToUTF16("GOODBYE")); + EXPECT_STR_EQ("GOODBYE", model.text()); + EXPECT_EQ(5U, model.cursor_pos()); + model.SelectAll(); + EXPECT_STR_EQ("GOODBYE", model.GetSelectedText()); + // Selection move the current pos to the begining. + EXPECT_EQ(0U, model.cursor_pos()); + model.MoveCursorToEnd(false); + EXPECT_EQ(7U, model.cursor_pos()); + + model.SetText(ASCIIToUTF16("BYE")); + EXPECT_EQ(3U, model.cursor_pos()); + EXPECT_EQ(string16(), model.GetSelectedText()); + model.SetText(ASCIIToUTF16("")); + EXPECT_EQ(0U, model.cursor_pos()); +} + +} // namespace views diff --git a/views/event.h b/views/event.h index 1ec666d..f878e2d 100644 --- a/views/event.h +++ b/views/event.h @@ -13,6 +13,7 @@ #if defined(OS_LINUX) typedef struct _GdkEventKey GdkEventKey; #endif + #if defined(TOUCH_UI) typedef union _XEvent XEvent; #endif @@ -332,7 +333,9 @@ class KeyEvent : public Event { int repeat_count, int message_flags); #if defined(OS_LINUX) - explicit KeyEvent(GdkEventKey* event); + explicit KeyEvent(const GdkEventKey* event); + + const GdkEventKey* native_event() const { return native_event_; } #endif #if defined(TOUCH_UI) @@ -366,7 +369,9 @@ class KeyEvent : public Event { app::KeyboardCode key_code_; int repeat_count_; int message_flags_; - +#if defined(OS_LINUX) + const GdkEventKey* native_event_; +#endif DISALLOW_COPY_AND_ASSIGN(KeyEvent); }; diff --git a/views/event_gtk.cc b/views/event_gtk.cc index cae426d..5e29ad9 100644 --- a/views/event_gtk.cc +++ b/views/event_gtk.cc @@ -10,14 +10,18 @@ namespace views { -KeyEvent::KeyEvent(GdkEventKey* event) +KeyEvent::KeyEvent(const GdkEventKey* event) : Event(event->type == GDK_KEY_PRESS ? Event::ET_KEY_PRESSED : Event::ET_KEY_RELEASED, GetFlagsFromGdkState(event->state)), // TODO(erg): All these values are iffy. key_code_(app::WindowsKeyCodeForGdkKeyCode(event->keyval)), repeat_count_(0), - message_flags_(0) { + message_flags_(0) +#if !defined(TOUCH_UI) + , native_event_(event) +#endif +{ } // static diff --git a/views/event_x.cc b/views/event_x.cc index 774aa7f..bddeb71 100644 --- a/views/event_x.cc +++ b/views/event_x.cc @@ -205,7 +205,8 @@ KeyEvent::KeyEvent(XEvent* xev) GetEventFlagsFromXState(xev->xkey.state)), key_code_(app::KeyboardCodeFromXKeyEvent(xev)), repeat_count_(0), - message_flags_(0) { + message_flags_(0), + native_event_(NULL) { } MouseEvent::MouseEvent(XEvent* xev) diff --git a/views/views.gyp b/views/views.gyp index 5cb7b55..35d12a8 100644 --- a/views/views.gyp +++ b/views/views.gyp @@ -223,11 +223,15 @@ 'controls/textfield/gtk_views_textview.h', 'controls/textfield/textfield.cc', 'controls/textfield/textfield.h', + 'controls/textfield/textfield_views_model.cc', + 'controls/textfield/textfield_views_model.h', 'controls/textfield/native_textfield_gtk.cc', 'controls/textfield/native_textfield_gtk.h', 'controls/textfield/native_textfield_win.cc', 'controls/textfield/native_textfield_win.h', 'controls/textfield/native_textfield_wrapper.h', + 'controls/textfield/native_textfield_views.cc', + 'controls/textfield/native_textfield_views.h', 'controls/throbber.cc', 'controls/throbber.h', 'controls/tree/tree_view.cc', @@ -392,6 +396,10 @@ 'controls/slider/slider.cc', 'controls/slider/slider.h', 'controls/slider/native_slider_wrapper.h', + 'controls/textfield/native_textfield_views.cc', + 'controls/textfield/native_textfield_views.h', + 'controls/textfield/textfield_views_model.cc', + 'controls/textfield/textfield_views_model.h', ], 'include_dirs': [ '<(DEPTH)/third_party/wtl/include', @@ -424,6 +432,8 @@ 'controls/progress_bar_unittest.cc', 'controls/tabbed_pane/tabbed_pane_unittest.cc', 'controls/table/table_view_unittest.cc', + 'controls/textfield/native_textfield_views_unittest.cc', + 'controls/textfield/textfield_views_model_unittest.cc', 'focus/accelerator_handler_gtk_unittest.cc', 'focus/focus_manager_unittest.cc', 'grid_layout_unittest.cc', @@ -454,6 +464,10 @@ # unrelated things like v8, sqlite nss...). '../chrome/app/locales/locales.gyp:en-US', ], + 'sources!': [ + 'controls/textfield/native_textfield_views_unittest.cc', + 'controls/textfield/textfield_views_model_unittest.cc', + ], 'link_settings': { 'libraries': [ '-limm32.lib', diff --git a/views/widget/root_view.cc b/views/widget/root_view.cc index a0a752c..82f32ba 100644 --- a/views/widget/root_view.cc +++ b/views/widget/root_view.cc @@ -22,6 +22,7 @@ #if defined(OS_LINUX) #include "views/widget/widget_gtk.h" +#include "views/controls/textfield/native_textfield_views.h" #endif // defined(OS_LINUX) namespace views { @@ -617,11 +618,13 @@ View* RootView::GetFocusedView() { View* view = focus_manager->GetFocusedView(); if (view && (view->GetRootView() == this)) return view; -#if defined(TOUCH_UI) - // hack to deal with two root views in touch - // should be fixed by eliminating one of them - if (view) + +#if defined(OS_LINUX) + if (view && NativeTextfieldViews::IsTextfieldViewsEnabled()) { + // hack to deal with two root views. + // should be fixed by eliminating one of them return view; + } #endif return NULL; } |