summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorandresantoso@chromium.org <andresantoso@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-06-25 20:46:27 +0000
committerandresantoso@chromium.org <andresantoso@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-06-25 20:46:27 +0000
commit0282b03c86d52533c58af2c6e8de4c7bc7a89155 (patch)
treee30c93979caaea0903342521717488a7522af61a
parentd0fe478040d70a386c8d43812e06b0396713cd9c (diff)
downloadchromium_src-0282b03c86d52533c58af2c6e8de4c7bc7a89155.zip
chromium_src-0282b03c86d52533c58af2c6e8de4c7bc7a89155.tar.gz
chromium_src-0282b03c86d52533c58af2c6e8de4c7bc7a89155.tar.bz2
MacViews: Implement text input.
Implement text input for MacViews by making BridgedContentView conform to NSTextInputClient protocol. Keyboard events are sent to the system input method to be interpreted, and the resulting NSTextInputClient calls are translated and forwarded to the ui::TextInputClient of the widget's focused View. BUG=374077 TEST=BridgedNativeWidgetTest Review URL: https://codereview.chromium.org/329463002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@279806 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--ui/views/cocoa/bridged_content_view.h11
-rw-r--r--ui/views/cocoa/bridged_content_view.mm134
-rw-r--r--ui/views/cocoa/bridged_native_widget.h15
-rw-r--r--ui/views/cocoa/bridged_native_widget.mm36
-rw-r--r--ui/views/cocoa/bridged_native_widget_unittest.mm206
-rw-r--r--ui/views/widget/native_widget_mac.mm2
6 files changed, 395 insertions, 9 deletions
diff --git a/ui/views/cocoa/bridged_content_view.h b/ui/views/cocoa/bridged_content_view.h
index e7c83f7..25fb364 100644
--- a/ui/views/cocoa/bridged_content_view.h
+++ b/ui/views/cocoa/bridged_content_view.h
@@ -7,6 +7,10 @@
#import <Cocoa/Cocoa.h>
+namespace ui {
+class TextInputClient;
+}
+
namespace views {
class View;
}
@@ -14,13 +18,18 @@ class View;
// The NSView that sits as the root contentView of the NSWindow, whilst it has
// a views::RootView present. Bridges requests from Cocoa to the hosted
// views::View.
-@interface BridgedContentView : NSView {
+@interface BridgedContentView : NSView<NSTextInputClient> {
@private
// Weak. The hosted RootView, owned by hostedView_->GetWidget().
views::View* hostedView_;
+
+ // Weak. If non-null the TextInputClient of the currently focused View in the
+ // hierarchy rooted at |hostedView_|. Owned by the focused View.
+ ui::TextInputClient* textInputClient_;
}
@property(readonly, nonatomic) views::View* hostedView;
+@property(assign, nonatomic) ui::TextInputClient* textInputClient;
// Initialize the NSView -> views::View bridge. |viewToHost| must be non-NULL.
- (id)initWithView:(views::View*)viewToHost;
diff --git a/ui/views/cocoa/bridged_content_view.mm b/ui/views/cocoa/bridged_content_view.mm
index bd9a511..1f2ef37 100644
--- a/ui/views/cocoa/bridged_content_view.mm
+++ b/ui/views/cocoa/bridged_content_view.mm
@@ -5,12 +5,25 @@
#import "ui/views/cocoa/bridged_content_view.h"
#include "base/logging.h"
+#include "base/strings/sys_string_conversions.h"
+#include "grit/ui_strings.h"
+#include "ui/base/ime/text_input_client.h"
#include "ui/gfx/canvas_paint_mac.h"
+#include "ui/gfx/geometry/rect.h"
#include "ui/views/view.h"
+@interface BridgedContentView ()
+
+// Execute a command on the currently focused TextInputClient.
+// |commandId| should be a resource ID from ui_strings.grd.
+- (void)doCommandByID:(int)commandId;
+
+@end
+
@implementation BridgedContentView
@synthesize hostedView = hostedView_;
+@synthesize textInputClient = textInputClient_;
- (id)initWithView:(views::View*)viewToHost {
DCHECK(viewToHost);
@@ -29,6 +42,13 @@
hostedView_ = NULL;
}
+// BridgedContentView private implementation.
+
+- (void)doCommandByID:(int)commandId {
+ if (textInputClient_ && textInputClient_->IsEditingCommandEnabled(commandId))
+ textInputClient_->ExecuteEditingCommand(commandId);
+}
+
// NSView implementation.
- (void)setFrameSize:(NSSize)newSize {
@@ -47,4 +67,118 @@
hostedView_->Paint(&canvas, views::CullSet());
}
+- (void)keyDown:(NSEvent*)theEvent {
+ if (textInputClient_)
+ [self interpretKeyEvents:@[ theEvent ]];
+ else
+ [super keyDown:theEvent];
+}
+
+- (void)deleteBackward:(id)sender {
+ [self doCommandByID:IDS_DELETE_BACKWARD];
+}
+
+- (void)deleteForward:(id)sender {
+ [self doCommandByID:IDS_DELETE_FORWARD];
+}
+
+- (void)moveLeft:(id)sender {
+ [self doCommandByID:IDS_MOVE_LEFT];
+}
+
+- (void)moveRight:(id)sender {
+ [self doCommandByID:IDS_MOVE_RIGHT];
+}
+
+// NSTextInputClient protocol implementation.
+
+- (NSAttributedString*)
+ attributedSubstringForProposedRange:(NSRange)range
+ actualRange:(NSRangePointer)actualRange {
+ base::string16 substring;
+ if (textInputClient_) {
+ gfx::Range textRange;
+ textInputClient_->GetTextRange(&textRange);
+ gfx::Range subrange = textRange.Intersect(gfx::Range(range));
+ textInputClient_->GetTextFromRange(subrange, &substring);
+ if (actualRange)
+ *actualRange = subrange.ToNSRange();
+ }
+ return [[[NSAttributedString alloc]
+ initWithString:base::SysUTF16ToNSString(substring)] autorelease];
+}
+
+- (NSUInteger)characterIndexForPoint:(NSPoint)aPoint {
+ NOTIMPLEMENTED();
+ return 0;
+}
+
+- (void)doCommandBySelector:(SEL)selector {
+ if ([self respondsToSelector:selector])
+ [self performSelector:selector withObject:nil];
+ else
+ [[self nextResponder] doCommandBySelector:selector];
+}
+
+- (NSRect)firstRectForCharacterRange:(NSRange)range
+ actualRange:(NSRangePointer)actualRange {
+ NOTIMPLEMENTED();
+ return NSZeroRect;
+}
+
+- (BOOL)hasMarkedText {
+ return textInputClient_ && textInputClient_->HasCompositionText();
+}
+
+- (void)insertText:(id)text replacementRange:(NSRange)replacementRange {
+ if (!textInputClient_)
+ return;
+
+ if ([text isKindOfClass:[NSAttributedString class]])
+ text = [text string];
+ textInputClient_->DeleteRange(gfx::Range(replacementRange));
+ textInputClient_->InsertText(base::SysNSStringToUTF16(text));
+}
+
+- (NSRange)markedRange {
+ if (!textInputClient_)
+ return NSMakeRange(NSNotFound, 0);
+
+ gfx::Range range;
+ textInputClient_->GetCompositionTextRange(&range);
+ return range.ToNSRange();
+}
+
+- (NSRange)selectedRange {
+ if (!textInputClient_)
+ return NSMakeRange(NSNotFound, 0);
+
+ gfx::Range range;
+ textInputClient_->GetSelectionRange(&range);
+ return range.ToNSRange();
+}
+
+- (void)setMarkedText:(id)text
+ selectedRange:(NSRange)selectedRange
+ replacementRange:(NSRange)replacementRange {
+ if (!textInputClient_)
+ return;
+
+ if ([text isKindOfClass:[NSAttributedString class]])
+ text = [text string];
+ ui::CompositionText composition;
+ composition.text = base::SysNSStringToUTF16(text);
+ composition.selection = gfx::Range(selectedRange);
+ textInputClient_->SetCompositionText(composition);
+}
+
+- (void)unmarkText {
+ if (textInputClient_)
+ textInputClient_->ConfirmCompositionText();
+}
+
+- (NSArray*)validAttributesForMarkedText {
+ return @[];
+}
+
@end
diff --git a/ui/views/cocoa/bridged_native_widget.h b/ui/views/cocoa/bridged_native_widget.h
index 3e21bbf..64cf81d 100644
--- a/ui/views/cocoa/bridged_native_widget.h
+++ b/ui/views/cocoa/bridged_native_widget.h
@@ -9,6 +9,7 @@
#import "base/mac/scoped_nsobject.h"
#include "base/memory/scoped_ptr.h"
+#import "ui/views/focus/focus_manager.h"
#include "ui/views/ime/input_method_delegate.h"
#include "ui/views/views_export.h"
#include "ui/views/widget/widget.h"
@@ -29,7 +30,8 @@ class View;
// A bridge to an NSWindow managed by an instance of NativeWidgetMac or
// DesktopNativeWidgetMac. Serves as a helper class to bridge requests from the
// NativeWidgetMac to the Cocoa window. Behaves a bit like an aura::Window.
-class VIEWS_EXPORT BridgedNativeWidget : public internal::InputMethodDelegate {
+class VIEWS_EXPORT BridgedNativeWidget : public internal::InputMethodDelegate,
+ public FocusChangeListener {
public:
// Creates one side of the bridge. |parent| must not be NULL.
explicit BridgedNativeWidget(NativeWidgetMac* parent);
@@ -39,6 +41,10 @@ class VIEWS_EXPORT BridgedNativeWidget : public internal::InputMethodDelegate {
void Init(base::scoped_nsobject<NSWindow> window,
const Widget::InitParams& params);
+ // Sets or clears the focus manager to use for tracking focused views.
+ // This does NOT take ownership of |focus_manager|.
+ void SetFocusManager(FocusManager* focus_manager);
+
// Set or clears the views::View bridged by the content view. This does NOT
// take ownership of |view|.
void SetRootView(views::View* view);
@@ -66,6 +72,13 @@ class VIEWS_EXPORT BridgedNativeWidget : public internal::InputMethodDelegate {
base::scoped_nsobject<ViewsNSWindowDelegate> window_delegate_;
base::scoped_nsobject<BridgedContentView> bridged_view_;
scoped_ptr<ui::InputMethod> input_method_;
+ FocusManager* focus_manager_; // Weak. Owned by our Widget.
+
+ // Overridden from FocusChangeListener:
+ virtual void OnWillChangeFocus(View* focused_before,
+ View* focused_now) OVERRIDE;
+ virtual void OnDidChangeFocus(View* focused_before,
+ View* focused_now) OVERRIDE;
DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidget);
};
diff --git a/ui/views/cocoa/bridged_native_widget.mm b/ui/views/cocoa/bridged_native_widget.mm
index 7695a66..2a959da 100644
--- a/ui/views/cocoa/bridged_native_widget.mm
+++ b/ui/views/cocoa/bridged_native_widget.mm
@@ -14,11 +14,12 @@
#include "ui/views/ime/input_method_bridge.h"
#include "ui/views/ime/null_input_method.h"
#include "ui/views/view.h"
+#include "ui/views/widget/widget.h"
namespace views {
BridgedNativeWidget::BridgedNativeWidget(NativeWidgetMac* parent)
- : native_widget_mac_(parent) {
+ : native_widget_mac_(parent), focus_manager_(NULL) {
DCHECK(parent);
window_delegate_.reset(
[[ViewsNSWindowDelegate alloc] initWithBridgedNativeWidget:this]);
@@ -26,6 +27,7 @@ BridgedNativeWidget::BridgedNativeWidget(NativeWidgetMac* parent)
BridgedNativeWidget::~BridgedNativeWidget() {
RemoveOrDestroyChildren();
+ SetFocusManager(NULL);
SetRootView(NULL);
if ([window_ delegate]) {
// If the delegate is still set, it means OnWindowWillClose has not been
@@ -49,6 +51,19 @@ void BridgedNativeWidget::Init(base::scoped_nsobject<NSWindow> window,
}
}
+void BridgedNativeWidget::SetFocusManager(FocusManager* focus_manager) {
+ if (focus_manager_ == focus_manager)
+ return;
+
+ if (focus_manager_)
+ focus_manager_->RemoveFocusChangeListener(this);
+
+ if (focus_manager)
+ focus_manager->AddFocusChangeListener(this);
+
+ focus_manager_ = focus_manager;
+}
+
void BridgedNativeWidget::SetRootView(views::View* view) {
if (view == [bridged_view_ hostedView])
return;
@@ -96,10 +111,21 @@ ui::InputMethod* BridgedNativeWidget::GetHostInputMethod() {
void BridgedNativeWidget::DispatchKeyEventPostIME(const ui::KeyEvent& key) {
// Mac key events don't go through this, but some unit tests that use
// MockInputMethod do.
- Widget* widget = [bridged_view_ hostedView]->GetWidget();
- widget->OnKeyEvent(const_cast<ui::KeyEvent*>(&key));
- if (!key.handled() && widget->GetFocusManager())
- widget->GetFocusManager()->OnKeyEvent(key);
+ DCHECK(focus_manager_);
+ native_widget_mac_->GetWidget()->OnKeyEvent(const_cast<ui::KeyEvent*>(&key));
+ if (!key.handled())
+ focus_manager_->OnKeyEvent(key);
+}
+
+void BridgedNativeWidget::OnWillChangeFocus(View* focused_before,
+ View* focused_now) {
+}
+
+void BridgedNativeWidget::OnDidChangeFocus(View* focused_before,
+ View* focused_now) {
+ ui::TextInputClient* input_client =
+ focused_now ? focused_now->GetTextInputClient() : NULL;
+ [bridged_view_ setTextInputClient:input_client];
}
////////////////////////////////////////////////////////////////////////////////
diff --git a/ui/views/cocoa/bridged_native_widget_unittest.mm b/ui/views/cocoa/bridged_native_widget_unittest.mm
index 22107d7..daba534 100644
--- a/ui/views/cocoa/bridged_native_widget_unittest.mm
+++ b/ui/views/cocoa/bridged_native_widget_unittest.mm
@@ -7,15 +7,36 @@
#import <Cocoa/Cocoa.h>
#include "base/memory/scoped_ptr.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/strings/utf_string_conversions.h"
#import "testing/gtest_mac.h"
#import "ui/gfx/test/ui_cocoa_test_helper.h"
#import "ui/views/cocoa/bridged_content_view.h"
+#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/ime/input_method.h"
#include "ui/views/view.h"
#include "ui/views/widget/native_widget_mac.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
+using base::ASCIIToUTF16;
+using base::SysNSStringToUTF8;
+using base::SysNSStringToUTF16;
+using base::SysUTF8ToNSString;
+
+#define EXPECT_EQ_RANGE(a, b) \
+ EXPECT_EQ(a.location, b.location); \
+ EXPECT_EQ(a.length, b.length);
+
+namespace {
+
+// Empty range shortcut for readibility.
+NSRange EmptyRange() {
+ return NSMakeRange(NSNotFound, 0);
+}
+
+} // namespace
+
namespace views {
namespace test {
@@ -80,6 +101,13 @@ class BridgedNativeWidgetTest : public BridgedNativeWidgetTestBase {
BridgedNativeWidgetTest();
virtual ~BridgedNativeWidgetTest();
+ // Install a textfield in the view hierarchy and make it the text input
+ // client.
+ void InstallTextField(const std::string& text);
+
+ // Returns the current text as std::string.
+ std::string GetText();
+
// testing::Test:
virtual void SetUp() OVERRIDE;
virtual void TearDown() OVERRIDE;
@@ -87,6 +115,8 @@ class BridgedNativeWidgetTest : public BridgedNativeWidgetTestBase {
protected:
// TODO(tapted): Make this a EventCountView from widget_unittest.cc.
scoped_ptr<views::View> view_;
+ scoped_ptr<BridgedNativeWidget> bridge_;
+ BridgedContentView* ns_view_; // Weak. Owned by bridge_.
private:
DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetTest);
@@ -98,6 +128,20 @@ BridgedNativeWidgetTest::BridgedNativeWidgetTest() {
BridgedNativeWidgetTest::~BridgedNativeWidgetTest() {
}
+void BridgedNativeWidgetTest::InstallTextField(const std::string& text) {
+ Textfield* textfield = new Textfield();
+ textfield->SetText(ASCIIToUTF16(text));
+ view_->AddChildView(textfield);
+ [ns_view_ setTextInputClient:textfield];
+}
+
+std::string BridgedNativeWidgetTest::GetText() {
+ NSRange range = NSMakeRange(0, NSUIntegerMax);
+ NSAttributedString* text =
+ [ns_view_ attributedSubstringForProposedRange:range actualRange:NULL];
+ return SysNSStringToUTF8([text string]);
+}
+
void BridgedNativeWidgetTest::SetUp() {
BridgedNativeWidgetTestBase::SetUp();
@@ -110,6 +154,7 @@ void BridgedNativeWidgetTest::SetUp() {
// The delegate should exist before setting the root view.
EXPECT_TRUE([window delegate]);
bridge()->SetRootView(view_.get());
+ ns_view_ = bridge()->ns_view();
[test_window() makePretendKeyWindowAndSetFirstResponder:bridge()->ns_view()];
}
@@ -165,12 +210,12 @@ TEST_F(BridgedNativeWidgetTest, ViewSizeTracksWindow) {
EXPECT_EQ(kTestNewHeight, view_->height());
}
-TEST_F(BridgedNativeWidgetTest, CreateInputMethod) {
+TEST_F(BridgedNativeWidgetTest, CreateInputMethodShouldNotReturnNull) {
scoped_ptr<views::InputMethod> input_method(bridge()->CreateInputMethod());
EXPECT_TRUE(input_method);
}
-TEST_F(BridgedNativeWidgetTest, GetHostInputMethod) {
+TEST_F(BridgedNativeWidgetTest, GetHostInputMethodShouldNotReturnNull) {
EXPECT_TRUE(bridge()->GetHostInputMethod());
}
@@ -208,5 +253,162 @@ TEST_F(BridgedNativeWidgetInitTest, ParentWindowNotNativeWidgetMac) {
EXPECT_EQ(0u, [[test_window() childWindows] count]);
}
+// Test getting complete string using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_GetCompleteString) {
+ const std::string kTestString = "foo bar baz";
+ InstallTextField(kTestString);
+
+ NSRange range = NSMakeRange(0, kTestString.size());
+ NSRange actual_range;
+ NSAttributedString* text =
+ [ns_view_ attributedSubstringForProposedRange:range
+ actualRange:&actual_range];
+ EXPECT_EQ(kTestString, SysNSStringToUTF8([text string]));
+ EXPECT_EQ_RANGE(range, actual_range);
+}
+
+// Test getting middle substring using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_GetMiddleSubstring) {
+ const std::string kTestString = "foo bar baz";
+ InstallTextField(kTestString);
+
+ NSRange range = NSMakeRange(4, 3);
+ NSRange actual_range;
+ NSAttributedString* text =
+ [ns_view_ attributedSubstringForProposedRange:range
+ actualRange:&actual_range];
+ EXPECT_EQ("bar", SysNSStringToUTF8([text string]));
+ EXPECT_EQ_RANGE(range, actual_range);
+}
+
+// Test getting ending substring using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_GetEndingSubstring) {
+ const std::string kTestString = "foo bar baz";
+ InstallTextField(kTestString);
+
+ NSRange range = NSMakeRange(8, 100);
+ NSRange actual_range;
+ NSAttributedString* text =
+ [ns_view_ attributedSubstringForProposedRange:range
+ actualRange:&actual_range];
+ EXPECT_EQ("baz", SysNSStringToUTF8([text string]));
+ EXPECT_EQ(range.location, actual_range.location);
+ EXPECT_EQ(3U, actual_range.length);
+}
+
+// Test getting empty substring using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_GetEmptySubstring) {
+ const std::string kTestString = "foo bar baz";
+ InstallTextField(kTestString);
+
+ NSRange range = EmptyRange();
+ NSRange actual_range;
+ NSAttributedString* text =
+ [ns_view_ attributedSubstringForProposedRange:range
+ actualRange:&actual_range];
+ EXPECT_EQ("", SysNSStringToUTF8([text string]));
+ EXPECT_EQ_RANGE(range, actual_range);
+}
+
+// Test inserting text using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_InsertText) {
+ const std::string kTestString = "foo";
+ InstallTextField(kTestString);
+
+ [ns_view_ insertText:SysUTF8ToNSString(kTestString)
+ replacementRange:EmptyRange()];
+ gfx::Range range(0, kTestString.size());
+ base::string16 text;
+ EXPECT_TRUE([ns_view_ textInputClient]->GetTextFromRange(range, &text));
+ EXPECT_EQ(ASCIIToUTF16(kTestString), text);
+}
+
+// Test replacing text using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_ReplaceText) {
+ const std::string kTestString = "foo bar";
+ InstallTextField(kTestString);
+
+ [ns_view_ insertText:@"baz" replacementRange:NSMakeRange(4, 3)];
+ EXPECT_EQ("foo baz", GetText());
+}
+
+// Test IME composition using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_Compose) {
+ const std::string kTestString = "foo ";
+ InstallTextField(kTestString);
+
+ EXPECT_FALSE([ns_view_ hasMarkedText]);
+ EXPECT_EQ_RANGE(EmptyRange(), [ns_view_ markedRange]);
+
+ // Start composition.
+ NSString* compositionText = @"bar";
+ NSUInteger compositionLength = [compositionText length];
+ [ns_view_ setMarkedText:compositionText
+ selectedRange:NSMakeRange(0, 2)
+ replacementRange:EmptyRange()];
+ EXPECT_TRUE([ns_view_ hasMarkedText]);
+ EXPECT_EQ_RANGE(NSMakeRange(kTestString.size(), compositionLength),
+ [ns_view_ markedRange]);
+ EXPECT_EQ_RANGE(NSMakeRange(kTestString.size(), 2), [ns_view_ selectedRange]);
+
+ // Confirm composition.
+ [ns_view_ unmarkText];
+ EXPECT_FALSE([ns_view_ hasMarkedText]);
+ EXPECT_EQ_RANGE(EmptyRange(), [ns_view_ markedRange]);
+ EXPECT_EQ("foo bar", GetText());
+ EXPECT_EQ_RANGE(NSMakeRange(GetText().size(), 0), [ns_view_ selectedRange]);
+}
+
+// Test moving the caret left and right using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_MoveLeftRight) {
+ InstallTextField("foo");
+ EXPECT_EQ_RANGE(NSMakeRange(3, 0), [ns_view_ selectedRange]);
+
+ // Move right not allowed, out of range.
+ [ns_view_ doCommandBySelector:@selector(moveRight:)];
+ EXPECT_EQ_RANGE(NSMakeRange(3, 0), [ns_view_ selectedRange]);
+
+ // Move left.
+ [ns_view_ doCommandBySelector:@selector(moveLeft:)];
+ EXPECT_EQ_RANGE(NSMakeRange(2, 0), [ns_view_ selectedRange]);
+
+ // Move right.
+ [ns_view_ doCommandBySelector:@selector(moveRight:)];
+ EXPECT_EQ_RANGE(NSMakeRange(3, 0), [ns_view_ selectedRange]);
+}
+
+// Test backward delete using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_DeleteBackward) {
+ InstallTextField("a");
+ EXPECT_EQ_RANGE(NSMakeRange(1, 0), [ns_view_ selectedRange]);
+
+ // Delete one character.
+ [ns_view_ doCommandBySelector:@selector(deleteBackward:)];
+ EXPECT_EQ("", GetText());
+ EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]);
+
+ // Try to delete again on an empty string.
+ [ns_view_ doCommandBySelector:@selector(deleteBackward:)];
+ EXPECT_EQ("", GetText());
+ EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]);
+}
+
+// Test forward delete using text input protocol.
+TEST_F(BridgedNativeWidgetTest, TextInput_DeleteForward) {
+ InstallTextField("a");
+ EXPECT_EQ_RANGE(NSMakeRange(1, 0), [ns_view_ selectedRange]);
+
+ // At the end of the string, can't delete forward.
+ [ns_view_ doCommandBySelector:@selector(deleteForward:)];
+ EXPECT_EQ("a", GetText());
+ EXPECT_EQ_RANGE(NSMakeRange(1, 0), [ns_view_ selectedRange]);
+
+ // Should succeed after moving left first.
+ [ns_view_ doCommandBySelector:@selector(moveLeft:)];
+ [ns_view_ doCommandBySelector:@selector(deleteForward:)];
+ EXPECT_EQ("", GetText());
+ EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]);
+}
+
} // namespace test
} // namespace views
diff --git a/ui/views/widget/native_widget_mac.mm b/ui/views/widget/native_widget_mac.mm
index c7f8108..184649b 100644
--- a/ui/views/widget/native_widget_mac.mm
+++ b/ui/views/widget/native_widget_mac.mm
@@ -63,6 +63,8 @@ void NativeWidgetMac::InitNativeWidget(const Widget::InitParams& params) {
bridge_->Init(window, params);
delegate_->OnNativeWidgetCreated(true);
+
+ bridge_->SetFocusManager(GetWidget()->GetFocusManager());
}
NonClientFrameView* NativeWidgetMac::CreateNonClientFrameView() {