diff options
author | avi@chromium.org <avi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-28 15:11:05 +0000 |
---|---|---|
committer | avi@chromium.org <avi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-28 15:11:05 +0000 |
commit | ae3fec8dc8d5c0559e495994c7e096354578bcab (patch) | |
tree | b304b634b657ec17fa7331856493a8dda8d5d7b7 /content/browser | |
parent | 66db7bc0efc03109bddc3b31f7944d7fe492f643 (diff) | |
download | chromium_src-ae3fec8dc8d5c0559e495994c7e096354578bcab.zip chromium_src-ae3fec8dc8d5c0559e495994c7e096354578bcab.tar.gz chromium_src-ae3fec8dc8d5c0559e495994c7e096354578bcab.tar.bz2 |
Move RenderWidgetHostViewMac to content.
BUG=95573
TEST=no visible change
Committed: http://src.chromium.org/viewvc/chrome?view=rev&revision=103113
Reverted: http://src.chromium.org/viewvc/chrome?view=rev&revision=103117
Review URL: http://codereview.chromium.org/8052026
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@103118 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'content/browser')
9 files changed, 4311 insertions, 0 deletions
diff --git a/content/browser/renderer_host/accelerated_plugin_view_mac.h b/content/browser/renderer_host/accelerated_plugin_view_mac.h new file mode 100644 index 0000000..f404c52 --- /dev/null +++ b/content/browser/renderer_host/accelerated_plugin_view_mac.h @@ -0,0 +1,58 @@ +// 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 CONTENT_BROWSER_RENDERER_HOST_ACCELERATED_PLUGIN_VIEW_MAC_H +#define CONTENT_BROWSER_RENDERER_HOST_ACCELERATED_PLUGIN_VIEW_MAC_H + +#import <Cocoa/Cocoa.h> +#include <QuartzCore/QuartzCore.h> + +#include "base/memory/scoped_nsobject.h" +#include "ui/gfx/native_widget_types.h" + +class RenderWidgetHostViewMac; + +// Informal protocol implemented by windows that need to be informed explicitly +// about underlay surfaces. +@interface NSObject (UnderlayableSurface) +- (void)underlaySurfaceAdded; +- (void)underlaySurfaceRemoved; +@end + +// This subclass of NSView hosts the output of accelerated plugins on +// the page. +@interface AcceleratedPluginView : NSView { + scoped_nsobject<NSOpenGLPixelFormat> glPixelFormat_; + CGLPixelFormatObj cglPixelFormat_; // weak, backed by |glPixelFormat_|. + scoped_nsobject<NSOpenGLContext> glContext_; + CGLContextObj cglContext_; // weak, backed by |glContext_|. + + RenderWidgetHostViewMac* renderWidgetHostView_; // weak + gfx::PluginWindowHandle pluginHandle_; // weak + + // Rects that should show web content rather than plugin content. + scoped_nsobject<NSArray> cutoutRects_; +} + +- (id)initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)r + pluginHandle:(gfx::PluginWindowHandle)pluginHandle; + +// Sets the list of rectangles that should show the web page, rather than the +// accelerated plugin. This is used to simulate the iframe-based trick that web +// pages have long used to show web content above windowed plugins on Windows +// and Linux. +- (void)setCutoutRects:(NSArray*)cutout_rects; + +// NSViews autorelease subviews when they die. The RWHVMac gets destroyed when +// RHWVCocoa gets dealloc'd, which means the AcceleratedPluginView child views +// can be around a little longer than the RWHVMac. This is called when the +// RWHVMac is about to be deleted (but it's still valid while this method runs). +- (void)onRenderWidgetHostViewGone; + +// Draw the view from the UI thread. +- (void)drawView; + +@end + +#endif // CONTENT_BROWSER_RENDERER_HOST_ACCELERATED_PLUGIN_VIEW_MAC_H diff --git a/content/browser/renderer_host/accelerated_plugin_view_mac.mm b/content/browser/renderer_host/accelerated_plugin_view_mac.mm new file mode 100644 index 0000000..9bcc433 --- /dev/null +++ b/content/browser/renderer_host/accelerated_plugin_view_mac.mm @@ -0,0 +1,247 @@ +// 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. + +#import "content/browser/renderer_host/accelerated_plugin_view_mac.h" + +#include "base/command_line.h" +#include "base/debug/trace_event.h" +#include "content/browser/browser_thread.h" +#include "content/browser/renderer_host/render_widget_host_view_mac.h" +#include "ui/gfx/gl/gl_switches.h" +#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" + +@implementation AcceleratedPluginView + +- (void)drawView { + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + TRACE_EVENT0("browser", "AcceleratedPluginViewMac::drawView"); + + if (renderWidgetHostView_) { + // TODO(thakis): Pixel or view coordinates for size? + renderWidgetHostView_->DrawAcceleratedSurfaceInstance( + cglContext_, pluginHandle_, [super frame].size); + } + + CGLFlushDrawable(cglContext_); + CGLSetCurrentContext(0); +} + +- (id)initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)r + pluginHandle:(gfx::PluginWindowHandle)pluginHandle { + if ((self = [super initWithFrame:NSZeroRect])) { + renderWidgetHostView_ = r; + pluginHandle_ = pluginHandle; + + [self setAutoresizingMask:NSViewMaxXMargin|NSViewMinYMargin]; + + NSOpenGLPixelFormatAttribute attributes[] = + { NSOpenGLPFAAccelerated, NSOpenGLPFADoubleBuffer, 0}; + + // TODO(zmo): remove the diagnostic error messages once we figure out the + // cause of the failure and a fix. + + glPixelFormat_.reset([[NSOpenGLPixelFormat alloc] + initWithAttributes:attributes]); + if (!glPixelFormat_) + LOG(ERROR) << "NSOpenGLPixelFormat initWithAttributes failed"; + + glContext_.reset([[NSOpenGLContext alloc] initWithFormat:glPixelFormat_ + shareContext:nil]); + if (!glContext_) + LOG(ERROR) << "NSOpenGLContext initWithFormat failed"; + + // We "punch a hole" in the window, and have the WindowServer render the + // OpenGL surface underneath so we can draw over it. + GLint belowWindow = -1; + [glContext_ setValues:&belowWindow forParameter:NSOpenGLCPSurfaceOrder]; + + cglContext_ = (CGLContextObj)[glContext_ CGLContextObj]; + if (!cglContext_) + LOG(ERROR) << "CGLContextObj failed"; + + cglPixelFormat_ = (CGLPixelFormatObj)[glPixelFormat_ CGLPixelFormatObj]; + if (!cglPixelFormat_) + LOG(ERROR) << "CGLPixelFormatObj failed"; + + // Draw at beam vsync. + GLint swapInterval; + if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kDisableGpuVsync)) + swapInterval = 0; + else + swapInterval = 1; + [glContext_ setValues:&swapInterval forParameter:NSOpenGLCPSwapInterval]; + + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(globalFrameDidChange:) + name:NSViewGlobalFrameDidChangeNotification + object:self]; + } + return self; +} + +- (void)dealloc { + if (renderWidgetHostView_) + renderWidgetHostView_->DeallocFakePluginWindowHandle(pluginHandle_); + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +- (void)setCutoutRects:(NSArray*)cutout_rects { + cutoutRects_.reset([cutout_rects copy]); +} + +- (void)onRenderWidgetHostViewGone { + if (!renderWidgetHostView_) + return; + + // Deallocate the plugin handle while we still can. + renderWidgetHostView_->DeallocFakePluginWindowHandle(pluginHandle_); + renderWidgetHostView_ = NULL; +} + +- (void)drawRect:(NSRect)rect { + TRACE_EVENT0("browser", "AcceleratedPluginView::drawRect"); + const NSRect* dirtyRects; + int dirtyRectCount; + [self getRectsBeingDrawn:&dirtyRects count:&dirtyRectCount]; + + { + gfx::ScopedNSGraphicsContextSaveGState scopedGState; + + // Mask out any cutout rects--somewhat counterintuitively cutout rects are + // places where clearColor is *not* drawn. The trick is that drawing nothing + // lets the parent view (i.e., the web page) show through, whereas drawing + // clearColor punches a hole in the window (letting OpenGL show through). + if ([cutoutRects_.get() count] > 0) { + NSBezierPath* path = [NSBezierPath bezierPath]; + // Trace the bounds clockwise to give a base clip rect of the whole view. + NSRect bounds = [self bounds]; + [path moveToPoint:bounds.origin]; + [path lineToPoint:NSMakePoint(NSMinX(bounds), NSMaxY(bounds))]; + [path lineToPoint:NSMakePoint(NSMaxX(bounds), NSMaxY(bounds))]; + [path lineToPoint:NSMakePoint(NSMaxX(bounds), NSMinY(bounds))]; + [path closePath]; + + // Then trace each cutout rect counterclockwise to remove that region from + // the clip region. + for (NSValue* rectWrapper in cutoutRects_.get()) { + [path appendBezierPathWithRect:[rectWrapper rectValue]]; + } + + [path addClip]; + } + + // Punch a hole so that the OpenGL view shows through. + [[NSColor clearColor] set]; + NSRectFillList(dirtyRects, dirtyRectCount); + } + + [self drawView]; +} + +- (void)rightMouseDown:(NSEvent*)event { + // The NSResponder documentation: "Note: The NSView implementation of this + // method does not pass the message up the responder chain, it handles it + // directly." + // That's bad, we want the next responder (RWHVMac) to handle this event to + // dispatch it to the renderer. + [[self nextResponder] rightMouseDown:event]; +} + +- (void)globalFrameDidChange:(NSNotification*)notification { + // This call to -update can call -globalFrameDidChange: again, see + // http://crbug.com/55754 comments 22 and 24. + [glContext_ update]; + + // You would think that -update updates the viewport. You would be wrong. + CGLSetCurrentContext(cglContext_); + NSSize size = [self frame].size; + glViewport(0, 0, size.width, size.height); + CGLSetCurrentContext(0); +} + +- (void)prepareForGLRendering { + TRACE_EVENT0("browser", "AcceleratedPluginView::prepareForGLRendering"); + + // If we're using OpenGL, make sure it is connected. + if ([glContext_ view] != self) { + [glContext_ setView:self]; + } +} + +- (void)renewGState { + TRACE_EVENT0("browser", "AcceleratedPluginView::renewGState"); + // Synchronize with window server to avoid flashes or corrupt drawing. + [[self window] disableScreenUpdatesUntilFlush]; + [self globalFrameDidChange:nil]; + [super renewGState]; +} + +- (void)setUpGState { + TRACE_EVENT0("browser", "AcceleratedPluginView::setUpGState"); + [self prepareForGLRendering]; +} + +// TODO(jbates): is lockFocus needed anymore? it seems redundant with +// setUpGState in traces. +- (void)lockFocus { + TRACE_EVENT0("browser", "AcceleratedPluginView::lockFocus"); + [super lockFocus]; + [self prepareForGLRendering]; + [glContext_ makeCurrentContext]; +} + +- (void)viewWillMoveToWindow:(NSWindow*)newWindow { + TRACE_EVENT1("browser", "AcceleratedPluginView::viewWillMoveToWindow", + "newWindow", newWindow); + // Inform the window hosting this accelerated view that it needs to be + // transparent. + if (![self isHiddenOrHasHiddenAncestor]) { + if ([[self window] respondsToSelector:@selector(underlaySurfaceRemoved)]) + [static_cast<id>([self window]) underlaySurfaceRemoved]; + if ([newWindow respondsToSelector:@selector(underlaySurfaceAdded)]) + [static_cast<id>(newWindow) underlaySurfaceAdded]; + } +} + +- (void)viewDidHide { + TRACE_EVENT0("browser", "AcceleratedPluginView::viewDidHide"); + [super viewDidHide]; + + if ([[self window] respondsToSelector:@selector(underlaySurfaceRemoved)]) { + [static_cast<id>([self window]) underlaySurfaceRemoved]; + } +} + +- (void)viewDidUnhide { + TRACE_EVENT0("browser", "AcceleratedPluginView::viewDidUnhide"); + [super viewDidUnhide]; + + if ([[self window] respondsToSelector:@selector(underlaySurfaceRemoved)]) { + [static_cast<id>([self window]) underlaySurfaceAdded]; + } +} + +- (BOOL)acceptsFirstResponder { + // Accept first responder if the first responder isn't the RWHVMac, and if the + // RWHVMac accepts first responder. If the RWHVMac does not accept first + // responder, do not accept on its behalf. + return ([[self window] firstResponder] != [self superview] && + [[self superview] acceptsFirstResponder]); +} + +- (BOOL)becomeFirstResponder { + // Delegate first responder to the RWHVMac. + [[self window] makeFirstResponder:[self superview]]; + return YES; +} + +- (void)viewDidMoveToSuperview { + TRACE_EVENT0("browser", "AcceleratedPluginView::viewDidMoveToSuperview"); + if (![self superview]) + [self onRenderWidgetHostViewGone]; +} +@end + diff --git a/content/browser/renderer_host/accelerated_plugin_view_mac_unittest.mm b/content/browser/renderer_host/accelerated_plugin_view_mac_unittest.mm new file mode 100644 index 0000000..4d902d2 --- /dev/null +++ b/content/browser/renderer_host/accelerated_plugin_view_mac_unittest.mm @@ -0,0 +1,188 @@ +// 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 "content/browser/renderer_host/accelerated_plugin_view_mac.h" + +#include "base/logging.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +@interface UnderlayCountingWindow : NSWindow { + @private + int underlayCount_; +} +@property (readonly, nonatomic) int underlayCount; +- (void)underlaySurfaceAdded; +- (void)underlaySurfaceRemoved; +@end + +@implementation UnderlayCountingWindow +@synthesize underlayCount = underlayCount_; + +- (void)underlaySurfaceAdded { + DCHECK_GE(underlayCount_, 0); + ++underlayCount_; +} + +- (void)underlaySurfaceRemoved { + --underlayCount_; + DCHECK_GE(underlayCount_, 0); +} +@end + +class AcceleratedPluginViewTest : public PlatformTest { + protected: + AcceleratedPluginViewTest() {} + + UnderlayCountingWindow* StubUnderlayWindow() { + UnderlayCountingWindow* window = [[[UnderlayCountingWindow alloc] + initWithContentRect:NSMakeRect(20, 20, 300, 200) + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO] autorelease]; + [window orderFront:nil]; + return window; + } + + AcceleratedPluginView* StubAcceleratedPluginView() { + // It truns out the rwhv and the plugin handle are not necessary for + // this test, and AcceleratedPluginView doesn't crash without them. + AcceleratedPluginView* view = [[[AcceleratedPluginView alloc] + initWithRenderWidgetHostViewMac:NULL + pluginHandle:0] autorelease]; + return view; + } + + private: + DISALLOW_COPY_AND_ASSIGN(AcceleratedPluginViewTest); +}; + +TEST_F(AcceleratedPluginViewTest, Basic) { +} + +TEST_F(AcceleratedPluginViewTest, BasicAdding) { + AcceleratedPluginView* view = StubAcceleratedPluginView(); + + UnderlayCountingWindow* window = StubUnderlayWindow(); + EXPECT_EQ(0, [window underlayCount]); + + [[window contentView] addSubview:view]; + EXPECT_EQ(1, [window underlayCount]); + + [view removeFromSuperview]; + EXPECT_EQ(0, [window underlayCount]); +} + +TEST_F(AcceleratedPluginViewTest, MoveBetweenWindows) { + AcceleratedPluginView* view = StubAcceleratedPluginView(); + + UnderlayCountingWindow* window1 = StubUnderlayWindow(); + UnderlayCountingWindow* window2 = StubUnderlayWindow(); + EXPECT_EQ(0, [window1 underlayCount]); + EXPECT_EQ(0, [window2 underlayCount]); + + [[window1 contentView] addSubview:view]; + EXPECT_EQ(1, [window1 underlayCount]); + EXPECT_EQ(0, [window2 underlayCount]); + + [[window2 contentView] addSubview:view]; + EXPECT_EQ(0, [window1 underlayCount]); + EXPECT_EQ(1, [window2 underlayCount]); +} + +TEST_F(AcceleratedPluginViewTest, HiddenWhenAdding) { + AcceleratedPluginView* view = StubAcceleratedPluginView(); + [view setHidden:YES]; + + UnderlayCountingWindow* window = StubUnderlayWindow(); + EXPECT_EQ(0, [window underlayCount]); + + [[window contentView] addSubview:view]; + EXPECT_EQ(0, [window underlayCount]); + + [view setHidden:NO]; + EXPECT_EQ(1, [window underlayCount]); + + [view setHidden:YES]; + EXPECT_EQ(0, [window underlayCount]); + + [view removeFromSuperview]; + EXPECT_EQ(0, [window underlayCount]); +} + +TEST_F(AcceleratedPluginViewTest, HiddenAfterAdding) { + AcceleratedPluginView* view = StubAcceleratedPluginView(); + + UnderlayCountingWindow* window = StubUnderlayWindow(); + EXPECT_EQ(0, [window underlayCount]); + + [[window contentView] addSubview:view]; + EXPECT_EQ(1, [window underlayCount]); + + [view setHidden:YES]; + EXPECT_EQ(0, [window underlayCount]); + + [view setHidden:NO]; + EXPECT_EQ(1, [window underlayCount]); + + [view removeFromSuperview]; + EXPECT_EQ(0, [window underlayCount]); +} + +TEST_F(AcceleratedPluginViewTest, MoveBetweenWindowsWithHiding) { + AcceleratedPluginView* view = StubAcceleratedPluginView(); + [view setHidden:YES]; + + UnderlayCountingWindow* window1 = StubUnderlayWindow(); + UnderlayCountingWindow* window2 = StubUnderlayWindow(); + EXPECT_EQ(0, [window1 underlayCount]); + EXPECT_EQ(0, [window2 underlayCount]); + + [[window1 contentView] addSubview:view]; + EXPECT_EQ(0, [window1 underlayCount]); + EXPECT_EQ(0, [window2 underlayCount]); + + [view setHidden:NO]; + EXPECT_EQ(1, [window1 underlayCount]); + EXPECT_EQ(0, [window2 underlayCount]); + + // Move view while it's visible. + [[window2 contentView] addSubview:view]; + EXPECT_EQ(0, [window1 underlayCount]); + EXPECT_EQ(1, [window2 underlayCount]); + + [view setHidden:YES]; + EXPECT_EQ(0, [window1 underlayCount]); + EXPECT_EQ(0, [window2 underlayCount]); + + // Move view while it's hidden. + [[window1 contentView] addSubview:view]; + EXPECT_EQ(0, [window1 underlayCount]); + EXPECT_EQ(0, [window2 underlayCount]); + + [view setHidden:NO]; + EXPECT_EQ(1, [window1 underlayCount]); + EXPECT_EQ(0, [window2 underlayCount]); +} + +// Regression test for http://crbug.com/81737 +TEST_F(AcceleratedPluginViewTest, RemoveWithHiddenParent) { + AcceleratedPluginView* view = StubAcceleratedPluginView(); + + NSView* parent = + [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)] autorelease]; + [parent addSubview:view]; + + UnderlayCountingWindow* window = StubUnderlayWindow(); + EXPECT_EQ(0, [window underlayCount]); + + [[window contentView] addSubview:parent]; + EXPECT_EQ(1, [window underlayCount]); + + [parent setHidden:YES]; + EXPECT_EQ(0, [window underlayCount]); + + [parent removeFromSuperview]; + EXPECT_EQ(0, [window underlayCount]); +} diff --git a/content/browser/renderer_host/render_widget_host_view_mac.h b/content/browser/renderer_host/render_widget_host_view_mac.h new file mode 100644 index 0000000..92e48c0 --- /dev/null +++ b/content/browser/renderer_host/render_widget_host_view_mac.h @@ -0,0 +1,410 @@ +// 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 CONTENT_BROWSER_RENDERER_HOST_RENDER_WIDGET_HOST_VIEW_MAC_H_ +#define CONTENT_BROWSER_RENDERER_HOST_RENDER_WIDGET_HOST_VIEW_MAC_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +#include "base/memory/scoped_nsobject.h" +#include "base/memory/scoped_ptr.h" +#include "base/task.h" +#include "base/time.h" +#include "content/browser/accessibility/browser_accessibility_delegate_mac.h" +#include "content/browser/accessibility/browser_accessibility_manager.h" +#include "content/browser/renderer_host/accelerated_surface_container_manager_mac.h" +#include "content/browser/renderer_host/render_widget_host_view.h" +#include "content/common/edit_command.h" +#include "third_party/WebKit/Source/WebKit/chromium/public/WebCompositionUnderline.h" +#include "ui/base/cocoa/base_view.h" +#include "webkit/glue/webcursor.h" + +@class AcceleratedPluginView; +class RenderWidgetHostViewMac; +@class RenderWidgetHostViewMacDelegate; +class RenderWidgetHostViewMacEditCommandHelper; +@class ToolTip; + +@protocol RenderWidgetHostViewMacOwner +- (RenderWidgetHostViewMac*)renderWidgetHostViewMac; +@end + +// This is the view that lives in the Cocoa view hierarchy. In Windows-land, +// RenderWidgetHostViewWin is both the view and the delegate. We split the roles +// but that means that the view needs to own the delegate and will dispose of it +// when it's removed from the view system. +@interface RenderWidgetHostViewCocoa + : BaseView <RenderWidgetHostViewMacOwner, + NSTextInputClient, + BrowserAccessibilityDelegateCocoa> { + @private + scoped_ptr<RenderWidgetHostViewMac> renderWidgetHostView_; + RenderWidgetHostViewMacDelegate* delegate_; // weak + BOOL canBeKeyView_; + BOOL takesFocusOnlyOnMouseDown_; + BOOL closeOnDeactivate_; + scoped_ptr<RenderWidgetHostViewMacEditCommandHelper> editCommand_helper_; + + // These are part of the magic tooltip code from WebKit's WebHTMLView: + id trackingRectOwner_; // (not retained) + void *trackingRectUserData_; + NSTrackingRectTag lastToolTipTag_; + scoped_nsobject<NSString> toolTip_; + + // Is YES if there was a mouse-down as yet unbalanced with a mouse-up. + BOOL hasOpenMouseDown_; + + NSWindow* lastWindow_; // weak + + // Variables used by our implementaion of the NSTextInput protocol. + // An input method of Mac calls the methods of this protocol not only to + // notify an application of its status, but also to retrieve the status of + // the application. That is, an application cannot control an input method + // directly. + // This object keeps the status of a composition of the renderer and returns + // it when an input method asks for it. + // We need to implement Objective-C methods for the NSTextInput protocol. On + // the other hand, we need to implement a C++ method for an IPC-message + // handler which receives input-method events from the renderer. + + // Represents the input-method attributes supported by this object. + scoped_nsobject<NSArray> validAttributesForMarkedText_; + + // Indicates if we are currently handling a key down event. + BOOL handlingKeyDown_; + + // Indicates if there is any marked text. + BOOL hasMarkedText_; + + // Indicates if unmarkText is called or not when handling a keyboard + // event. + BOOL unmarkTextCalled_; + + // The range of current marked text inside the whole content of the DOM node + // being edited. + // TODO(suzhe): This is currently a fake value, as we do not support accessing + // the whole content yet. + NSRange markedRange_; + + // The selected range, cached from a message sent by the renderer. + NSRange selectedRange_; + + // Text to be inserted which was generated by handling a key down event. + string16 textToBeInserted_; + + // Marked text which was generated by handling a key down event. + string16 markedText_; + + // Underline information of the |markedText_|. + std::vector<WebKit::WebCompositionUnderline> underlines_; + + // Indicates if doCommandBySelector method receives any edit command when + // handling a key down event. + BOOL hasEditCommands_; + + // Contains edit commands received by the -doCommandBySelector: method when + // handling a key down event, not including inserting commands, eg. insertTab, + // etc. + EditCommands editCommands_; + + // The plugin that currently has focus (-1 if no plugin has focus). + int focusedPluginIdentifier_; + + // Whether or not plugin IME is currently enabled active. + BOOL pluginImeActive_; + + // Whether the previous mouse event was ignored due to hitTest check. + BOOL mouseEventWasIgnored_; + + // Event monitor for gesture-end events. + id endGestureMonitor_; +} + +@property(nonatomic, readonly) NSRange selectedRange; + +- (void)setCanBeKeyView:(BOOL)can; +- (void)setTakesFocusOnlyOnMouseDown:(BOOL)b; +- (void)setCloseOnDeactivate:(BOOL)b; +- (void)setToolTipAtMousePoint:(NSString *)string; +// Set frame, then notify the RenderWidgetHost that the frame has been changed, +// but do it in a separate task, using |performSelector:withObject:afterDelay:|. +// This stops the flickering issue in http://crbug.com/31970 +- (void)setFrameWithDeferredUpdate:(NSRect)frame; +// Notify the RenderWidgetHost that the frame was updated so it can resize +// its contents. +- (void)renderWidgetHostWasResized; +// Cancel ongoing composition (abandon the marked text). +- (void)cancelComposition; +// Confirm ongoing composition. +- (void)confirmComposition; +// Enables or disables plugin IME. +- (void)setPluginImeActive:(BOOL)active; +// Updates the current plugin focus state. +- (void)pluginFocusChanged:(BOOL)focused forPlugin:(int)pluginId; +// Evaluates the event in the context of plugin IME, if plugin IME is enabled. +// Returns YES if the event was handled. +- (BOOL)postProcessEventForPluginIme:(NSEvent*)event; + +@end + +/////////////////////////////////////////////////////////////////////////////// +// RenderWidgetHostViewMac +// +// An object representing the "View" of a rendered web page. This object is +// responsible for displaying the content of the web page, and integrating with +// the Cocoa view system. It is the implementation of the RenderWidgetHostView +// that the cross-platform RenderWidgetHost object uses +// to display the data. +// +// Comment excerpted from render_widget_host.h: +// +// "The lifetime of the RenderWidgetHost* is tied to the render process. +// If the render process dies, the RenderWidgetHost* goes away and all +// references to it must become NULL." +// +class RenderWidgetHostViewMac : public RenderWidgetHostView { + public: + // The view will associate itself with the given widget. The native view must + // be hooked up immediately to the view hierarchy, or else when it is + // deleted it will delete this out from under the caller. + explicit RenderWidgetHostViewMac(RenderWidgetHost* widget); + virtual ~RenderWidgetHostViewMac(); + + RenderWidgetHostViewCocoa* native_view() const { return cocoa_view_; } + + void SetDelegate(RenderWidgetHostViewMacDelegate* delegate); + + // Implementation of RenderWidgetHostView: + virtual void InitAsPopup(RenderWidgetHostView* parent_host_view, + const gfx::Rect& pos) OVERRIDE; + virtual void InitAsFullscreen( + RenderWidgetHostView* reference_host_view) OVERRIDE; + virtual RenderWidgetHost* GetRenderWidgetHost() const OVERRIDE; + virtual void DidBecomeSelected() OVERRIDE; + virtual void WasHidden() OVERRIDE; + virtual void SetSize(const gfx::Size& size) OVERRIDE; + virtual void SetBounds(const gfx::Rect& rect) OVERRIDE; + virtual gfx::NativeView GetNativeView() const OVERRIDE; + virtual gfx::NativeViewId GetNativeViewId() const OVERRIDE; + virtual void MovePluginWindows( + const std::vector<webkit::npapi::WebPluginGeometry>& moves) OVERRIDE; + virtual void Focus() OVERRIDE; + virtual void Blur() OVERRIDE; + virtual bool HasFocus() OVERRIDE; + virtual void Show() OVERRIDE; + virtual void Hide() OVERRIDE; + virtual bool IsShowing() OVERRIDE; + virtual gfx::Rect GetViewBounds() const OVERRIDE; + virtual void UpdateCursor(const WebCursor& cursor) OVERRIDE; + virtual void SetIsLoading(bool is_loading) OVERRIDE; + virtual void ImeUpdateTextInputState(ui::TextInputType state, + bool can_compose_inline, + const gfx::Rect& caret_rect) OVERRIDE; + virtual void ImeCancelComposition() OVERRIDE; + virtual void ImeCompositionRangeChanged(const ui::Range& range) OVERRIDE; + virtual void DidUpdateBackingStore( + const gfx::Rect& scroll_rect, int scroll_dx, int scroll_dy, + const std::vector<gfx::Rect>& copy_rects) OVERRIDE; + virtual void RenderViewGone(base::TerminationStatus status, + int error_code) OVERRIDE; + virtual void Destroy() OVERRIDE; + virtual void SetTooltipText(const string16& tooltip_text) OVERRIDE; + virtual void SelectionChanged(const std::string& text, + const ui::Range& range, + const gfx::Point& start, + const gfx::Point& end) OVERRIDE; + virtual void ShowingContextMenu(bool showing) OVERRIDE; + virtual BackingStore* AllocBackingStore(const gfx::Size& size) OVERRIDE; + virtual void SetTakesFocusOnlyOnMouseDown(bool flag) OVERRIDE; + // See comment in RenderWidgetHostView! + virtual gfx::Rect GetViewCocoaBounds() const OVERRIDE; + virtual void SetActive(bool active) OVERRIDE; + virtual void SetWindowVisibility(bool visible) OVERRIDE; + virtual void WindowFrameChanged() OVERRIDE; + virtual void SetBackground(const SkBitmap& background) OVERRIDE; + + virtual void OnAccessibilityNotifications( + const std::vector<ViewHostMsg_AccessibilityNotification_Params>& params + ) OVERRIDE; + + virtual void PluginFocusChanged(bool focused, int plugin_id) OVERRIDE; + virtual void StartPluginIme() OVERRIDE; + virtual bool PostProcessEventForPluginIme( + const NativeWebKeyboardEvent& event) OVERRIDE; + + // Methods associated with GPU-accelerated plug-in instances and the + // accelerated compositor. + virtual gfx::PluginWindowHandle AllocateFakePluginWindowHandle( + bool opaque, bool root) OVERRIDE; + virtual void DestroyFakePluginWindowHandle( + gfx::PluginWindowHandle window) OVERRIDE; + + // Exposed for testing. + AcceleratedPluginView* ViewForPluginWindowHandle( + gfx::PluginWindowHandle window); + + // Helper to do the actual cleanup after a plugin handle has been destroyed. + // Required because DestroyFakePluginWindowHandle() isn't always called for + // all handles (it's e.g. not called on navigation, when the RWHVMac gets + // destroyed anyway). + void DeallocFakePluginWindowHandle(gfx::PluginWindowHandle window); + + virtual void AcceleratedSurfaceSetIOSurface( + gfx::PluginWindowHandle window, + int32 width, + int32 height, + uint64 io_surface_identifier) OVERRIDE; + virtual void AcceleratedSurfaceSetTransportDIB( + gfx::PluginWindowHandle window, + int32 width, + int32 height, + TransportDIB::Handle transport_dib) OVERRIDE; + virtual void AcceleratedSurfaceBuffersSwapped( + gfx::PluginWindowHandle window, + uint64 surface_id, + int renderer_id, + int32 route_id, + int gpu_host_id, + uint64 swap_buffers_count) OVERRIDE; + virtual void GpuRenderingStateDidChange() OVERRIDE; + virtual void GetScreenInfo(WebKit::WebScreenInfo* results) OVERRIDE; + virtual gfx::Rect GetRootWindowBounds() OVERRIDE; + virtual gfx::PluginWindowHandle GetCompositingSurface() OVERRIDE; + + // Returns |true| if a context menu is currently being shown. + bool is_showing_context_menu() const { return is_showing_context_menu_; } + + void DrawAcceleratedSurfaceInstance( + CGLContextObj context, + gfx::PluginWindowHandle plugin_handle, + NSSize size); + // Forces the textures associated with any accelerated plugin instances + // to be reloaded. + void ForceTextureReload(); + + virtual void SetVisuallyDeemphasized(const SkColor* color, bool animate); + + virtual void UnhandledWheelEvent( + const WebKit::WebMouseWheelEvent& event) OVERRIDE; + virtual void SetHasHorizontalScrollbar( + bool has_horizontal_scrollbar) OVERRIDE; + virtual void SetScrollOffsetPinning( + bool is_pinned_to_left, bool is_pinned_to_right) OVERRIDE; + + virtual bool LockMouse() OVERRIDE; + virtual void UnlockMouse() OVERRIDE; + + void KillSelf(); + + void SetTextInputActive(bool active); + + // Sends completed plugin IME notification and text back to the renderer. + void PluginImeCompositionCompleted(const string16& text, int plugin_id); + + const std::string& selected_text() const { return selected_text_; } + + void UpdateRootGpuViewVisibility(bool show_gpu_widget); + + // When rendering transitions from gpu to software, the gpu widget can't be + // hidden until the software backing store has been updated. This method + // checks if the GPU view needs to be hidden and hides it if necessary. It + // should be called after the software backing store has been painted to. + void HandleDelayedGpuViewHiding(); + + // This is called from the display link thread, and provides the GPU + // process a notion of how quickly the browser is able to keep up with it. + void AcknowledgeSwapBuffers(int renderer_id, + int32 route_id, + int gpu_host_id, + uint64 swap_buffers_count); + + // These member variables should be private, but the associated ObjC class + // needs access to them and can't be made a friend. + + // The associated Model. Can be NULL if Destroy() is called when + // someone (other than superview) has retained |cocoa_view_|. + RenderWidgetHost* render_widget_host_; + + // This is true when we are currently painting and thus should handle extra + // paint requests by expanding the invalid rect rather than actually painting. + bool about_to_validate_and_paint_; + + scoped_ptr<BrowserAccessibilityManager> browser_accessibility_manager_; + + // This is true when we have already scheduled a call to + // |-callSetNeedsDisplayInRect:| but it has not been fulfilled yet. Used to + // prevent us from scheduling multiple calls. + bool call_set_needs_display_in_rect_pending_; + + // The invalid rect that needs to be painted by callSetNeedsDisplayInRect. + // This value is only meaningful when + // |call_set_needs_display_in_rect_pending_| is true. + NSRect invalid_rect_; + + // The time at which this view started displaying white pixels as a result of + // not having anything to paint (empty backing store from renderer). This + // value returns true for is_null() if we are not recording whiteout times. + base::TimeTicks whiteout_start_time_; + + // The time it took after this view was selected for it to be fully painted. + base::TimeTicks tab_switch_paint_time_; + + // Current text input type. + ui::TextInputType text_input_type_; + + typedef std::map<gfx::PluginWindowHandle, AcceleratedPluginView*> + PluginViewMap; + PluginViewMap plugin_views_; // Weak values. + + // Helper class for managing instances of accelerated plug-ins. + AcceleratedSurfaceContainerManagerMac plugin_container_manager_; + + private: + // Returns whether this render view is a popup (autocomplete window). + bool IsPopup() const; + + // Updates the display cursor if the current event is over the view's window. + void UpdateCursorIfNecessary(); + + // Shuts down the render_widget_host_. This is a separate function so we can + // invoke it from the message loop. + void ShutdownHost(); + + // The associated view. This is weak and is inserted into the view hierarchy + // to own this RenderWidgetHostViewMac object. + RenderWidgetHostViewCocoa* cocoa_view_; + + // The cursor for the page. This is passed up from the renderer. + WebCursor current_cursor_; + + // Indicates if the page is loading. + bool is_loading_; + + // true if the View is not visible. + bool is_hidden_; + + // Whether we are showing a context menu. + bool is_showing_context_menu_; + + // The text to be shown in the tooltip, supplied by the renderer. + string16 tooltip_text_; + + // Factory used to safely scope delayed calls to ShutdownHost(). + ScopedRunnableMethodFactory<RenderWidgetHostViewMac> shutdown_factory_; + + // selected text on the renderer. + std::string selected_text_; + + // When rendering transitions from gpu to software, the gpu widget can't be + // hidden until the software backing store has been updated. This variable is + // set when the gpu widget needs to be hidden once a paint is completed. + bool needs_gpu_visibility_update_after_repaint_; + + gfx::PluginWindowHandle compositing_surface_; + + DISALLOW_COPY_AND_ASSIGN(RenderWidgetHostViewMac); +}; + +#endif // CONTENT_BROWSER_RENDERER_HOST_RENDER_WIDGET_HOST_VIEW_MAC_H_ diff --git a/content/browser/renderer_host/render_widget_host_view_mac.mm b/content/browser/renderer_host/render_widget_host_view_mac.mm new file mode 100644 index 0000000..8f720a3 --- /dev/null +++ b/content/browser/renderer_host/render_widget_host_view_mac.mm @@ -0,0 +1,2740 @@ +// 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 "content/browser/renderer_host/render_widget_host_view_mac.h" + +#include <QuartzCore/QuartzCore.h> + +#include "base/debug/trace_event.h" +#include "base/logging.h" +#include "base/mac/mac_util.h" +#include "base/mac/scoped_cftyperef.h" +#import "base/mac/scoped_nsautorelease_pool.h" +#include "base/metrics/histogram.h" +#import "base/memory/scoped_nsobject.h" +#include "base/string_util.h" +#include "base/sys_info.h" +#include "base/sys_string_conversions.h" +#import "content/browser/accessibility/browser_accessibility_cocoa.h" +#include "content/browser/browser_thread.h" +#include "content/browser/gpu/gpu_process_host.h" +#include "content/browser/gpu/gpu_process_host_ui_shim.h" +#include "content/browser/mac/closure_blocks_leopard_compat.h" +#include "content/browser/plugin_process_host.h" +#import "content/browser/renderer_host/accelerated_plugin_view_mac.h" +#include "content/browser/renderer_host/backing_store_mac.h" +#include "content/browser/renderer_host/render_process_host.h" +#include "content/browser/renderer_host/render_view_host.h" +#import "content/browser/renderer_host/render_widget_host_view_mac_delegate.h" +#import "content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.h" +#import "content/browser/renderer_host/text_input_client_mac.h" +#include "content/common/edit_command.h" +#include "content/common/gpu/gpu_messages.h" +#include "content/common/native_web_keyboard_event.h" +#include "content/common/plugin_messages.h" +#include "content/common/view_messages.h" +#include "skia/ext/platform_canvas.h" +#import "third_party/mozilla/ComplexTextInputPanel.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/WebKit/Source/WebKit/chromium/public/WebInputEvent.h" +#include "third_party/WebKit/Source/WebKit/chromium/public/WebScreenInfo.h" +#include "third_party/WebKit/Source/WebKit/chromium/public/mac/WebInputEventFactory.h" +#include "third_party/WebKit/Source/WebKit/chromium/public/mac/WebScreenInfoFactory.h" +#include "ui/gfx/point.h" +#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" +#include "ui/gfx/surface/io_surface_support_mac.h" +#include "webkit/glue/webaccessibility.h" +#include "webkit/plugins/npapi/webplugin.h" + +using WebKit::WebInputEvent; +using WebKit::WebInputEventFactory; +using WebKit::WebMouseEvent; +using WebKit::WebMouseWheelEvent; +using WebKit::WebGestureEvent; + +// Declare things that are part of the 10.6 SDK. +#if !defined(MAC_OS_X_VERSION_10_6) || \ + MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_6 +enum { + NSEventTypeBeginGesture = 19, + NSEventTypeEndGesture = 20 +}; + +enum { + NSEventMaskBeginGesture = 1 << NSEventTypeBeginGesture, + NSEventMaskEndGesture = 1 << NSEventTypeEndGesture, +}; + +typedef unsigned long long NSEventMask; + +@class NSTextInputContext; +@interface NSResponder (AppKitDetails) +- (NSTextInputContext*)inputContext; +@end +#endif // 10.6 + +// Declare things that are part of the 10.7 SDK. +#if !defined(MAC_OS_X_VERSION_10_7) || \ + MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7 +@interface NSEvent (LionAPI) ++ (id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask + handler:(NSEvent* (^)(NSEvent*))block; ++ (void)removeMonitor:(id)eventMonitor; +@end +#endif // 10.7 + +static inline int ToWebKitModifiers(NSUInteger flags) { + int modifiers = 0; + if (flags & NSControlKeyMask) modifiers |= WebInputEvent::ControlKey; + if (flags & NSShiftKeyMask) modifiers |= WebInputEvent::ShiftKey; + if (flags & NSAlternateKeyMask) modifiers |= WebInputEvent::AltKey; + if (flags & NSCommandKeyMask) modifiers |= WebInputEvent::MetaKey; + return modifiers; +} + +// Private methods: +@interface RenderWidgetHostViewCocoa () +@property(nonatomic, assign) NSRange selectedRange; +@property(nonatomic, assign) NSRange markedRange; + ++ (BOOL)shouldAutohideCursorForEvent:(NSEvent*)event; +- (id)initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)r; +- (void)setRWHVDelegate:(RenderWidgetHostViewMacDelegate*)delegate; +- (void)gotUnhandledWheelEvent; +- (void)scrollOffsetPinnedToLeft:(BOOL)left toRight:(BOOL)right; +- (void)setHasHorizontalScrollbar:(BOOL)has_horizontal_scrollbar; +- (void)keyEvent:(NSEvent*)theEvent wasKeyEquivalent:(BOOL)equiv; +- (void)cancelChildPopups; +- (void)checkForPluginImeCancellation; +@end + +// NSEvent subtype for scroll gestures events. +static const short kIOHIDEventTypeScroll = 6; + +namespace { + +// Maximum number of characters we allow in a tooltip. +const size_t kMaxTooltipLength = 1024; + +// TODO(suzhe): Upstream this function. +WebKit::WebColor WebColorFromNSColor(NSColor *color) { + CGFloat r, g, b, a; + [color getRed:&r green:&g blue:&b alpha:&a]; + + return + std::max(0, std::min(static_cast<int>(lroundf(255.0f * a)), 255)) << 24 | + std::max(0, std::min(static_cast<int>(lroundf(255.0f * r)), 255)) << 16 | + std::max(0, std::min(static_cast<int>(lroundf(255.0f * g)), 255)) << 8 | + std::max(0, std::min(static_cast<int>(lroundf(255.0f * b)), 255)); +} + +// Extract underline information from an attributed string. Mostly copied from +// third_party/WebKit/Source/WebKit/mac/WebView/WebHTMLView.mm +void ExtractUnderlines( + NSAttributedString* string, + std::vector<WebKit::WebCompositionUnderline>* underlines) { + int length = [[string string] length]; + int i = 0; + while (i < length) { + NSRange range; + NSDictionary* attrs = [string attributesAtIndex:i + longestEffectiveRange:&range + inRange:NSMakeRange(i, length - i)]; + if (NSNumber *style = [attrs objectForKey:NSUnderlineStyleAttributeName]) { + WebKit::WebColor color = SK_ColorBLACK; + if (NSColor *colorAttr = + [attrs objectForKey:NSUnderlineColorAttributeName]) { + color = WebColorFromNSColor( + [colorAttr colorUsingColorSpaceName:NSDeviceRGBColorSpace]); + } + underlines->push_back(WebKit::WebCompositionUnderline( + range.location, NSMaxRange(range), color, [style intValue] > 1)); + } + i = range.location + range.length; + } +} + +// EnablePasswordInput() and DisablePasswordInput() are copied from +// enableSecureTextInput() and disableSecureTextInput() functions in +// third_party/WebKit/WebCore/platform/SecureTextInput.cpp +// But we don't call EnableSecureEventInput() and DisableSecureEventInput() +// here, because they are already called in webkit and they are system wide +// functions. +void EnablePasswordInput() { + CFArrayRef inputSources = TISCreateASCIICapableInputSourceList(); + TSMSetDocumentProperty(0, kTSMDocumentEnabledInputSourcesPropertyTag, + sizeof(CFArrayRef), &inputSources); + CFRelease(inputSources); +} + +void DisablePasswordInput() { + TSMRemoveDocumentProperty(0, kTSMDocumentEnabledInputSourcesPropertyTag); +} + +// Adjusts an NSRect in Cocoa screen coordinates to have an origin in the upper +// left of the primary screen (Carbon coordinates), and stuffs it into a +// gfx::Rect. +gfx::Rect FlipNSRectToRectScreen(const NSRect& rect) { + gfx::Rect new_rect(NSRectToCGRect(rect)); + if ([[NSScreen screens] count] > 0) { + new_rect.set_y([[[NSScreen screens] objectAtIndex:0] frame].size.height - + new_rect.y() - new_rect.height()); + } + return new_rect; +} + +// Returns the window that visually contains the given view. This is different +// from [view window] in the case of tab dragging, where the view's owning +// window is a floating panel attached to the actual browser window that the tab +// is visually part of. +NSWindow* ApparentWindowForView(NSView* view) { + // TODO(shess): In case of !window, the view has been removed from + // the view hierarchy because the tab isn't main. Could retrieve + // the information from the main tab for our window. + NSWindow* enclosing_window = [view window]; + + // See if this is a tab drag window. The width check is to distinguish that + // case from extension popup windows. + NSWindow* ancestor_window = [enclosing_window parentWindow]; + if (ancestor_window && (NSWidth([enclosing_window frame]) == + NSWidth([ancestor_window frame]))) { + enclosing_window = ancestor_window; + } + + return enclosing_window; +} + +} // namespace + +// RenderWidgetHostView -------------------------------------------------------- + +// static +RenderWidgetHostView* RenderWidgetHostView::CreateViewForWidget( + RenderWidgetHost* widget) { + return new RenderWidgetHostViewMac(widget); +} + +/////////////////////////////////////////////////////////////////////////////// +// RenderWidgetHostViewMac, public: + +RenderWidgetHostViewMac::RenderWidgetHostViewMac(RenderWidgetHost* widget) + : render_widget_host_(widget), + about_to_validate_and_paint_(false), + call_set_needs_display_in_rect_pending_(false), + text_input_type_(ui::TEXT_INPUT_TYPE_NONE), + is_loading_(false), + is_hidden_(false), + is_showing_context_menu_(false), + shutdown_factory_(this), + needs_gpu_visibility_update_after_repaint_(false), + compositing_surface_(gfx::kNullPluginWindow) { + // |cocoa_view_| owns us and we will be deleted when |cocoa_view_| goes away. + // Since we autorelease it, our caller must put |native_view()| into the view + // hierarchy right after calling us. + cocoa_view_ = [[[RenderWidgetHostViewCocoa alloc] + initWithRenderWidgetHostViewMac:this] autorelease]; + render_widget_host_->SetView(this); +} + +RenderWidgetHostViewMac::~RenderWidgetHostViewMac() { +} + +void RenderWidgetHostViewMac::SetDelegate( + RenderWidgetHostViewMacDelegate* delegate) { + [cocoa_view_ setRWHVDelegate:delegate]; +} + +/////////////////////////////////////////////////////////////////////////////// +// RenderWidgetHostViewMac, RenderWidgetHostView implementation: + +void RenderWidgetHostViewMac::InitAsPopup( + RenderWidgetHostView* parent_host_view, + const gfx::Rect& pos) { + bool activatable = popup_type_ == WebKit::WebPopupTypeNone; + [cocoa_view_ setCloseOnDeactivate:YES]; + [cocoa_view_ setCanBeKeyView:activatable ? YES : NO]; + [parent_host_view->GetNativeView() addSubview:cocoa_view_]; + + NSPoint origin_global = NSPointFromCGPoint(pos.origin().ToCGPoint()); + if ([[NSScreen screens] count] > 0) { + origin_global.y = [[[NSScreen screens] objectAtIndex:0] frame].size.height - + pos.height() - origin_global.y; + } + NSPoint origin_window = + [[cocoa_view_ window] convertScreenToBase:origin_global]; + NSPoint origin_view = + [cocoa_view_ convertPoint:origin_window fromView:nil]; + NSRect initial_frame = NSMakeRect(origin_view.x, + origin_view.y, + pos.width(), + pos.height()); + [cocoa_view_ setFrame:initial_frame]; +} + +void RenderWidgetHostViewMac::InitAsFullscreen( + RenderWidgetHostView* /*reference_host_view*/) { + NOTIMPLEMENTED() << "Full screen not implemented on Mac"; +} + +RenderWidgetHost* RenderWidgetHostViewMac::GetRenderWidgetHost() const { + return render_widget_host_; +} + +void RenderWidgetHostViewMac::DidBecomeSelected() { + if (!is_hidden_) + return; + + if (tab_switch_paint_time_.is_null()) + tab_switch_paint_time_ = base::TimeTicks::Now(); + is_hidden_ = false; + render_widget_host_->WasRestored(); +} + +void RenderWidgetHostViewMac::WasHidden() { + if (is_hidden_) + return; + + // If we receive any more paint messages while we are hidden, we want to + // ignore them so we don't re-allocate the backing store. We will paint + // everything again when we become selected again. + is_hidden_ = true; + + // If we have a renderer, then inform it that we are being hidden so it can + // reduce its resource utilization. + render_widget_host_->WasHidden(); +} + +void RenderWidgetHostViewMac::SetSize(const gfx::Size& size) { + gfx::Rect rect = GetViewBounds(); + rect.set_size(size); + SetBounds(rect); +} + +void RenderWidgetHostViewMac::SetBounds(const gfx::Rect& rect) { + // |rect.size()| is view coordinates, |rect.origin| is screen coordinates, + // TODO(thakis): fix, http://crbug.com/73362 + if (is_hidden_) + return; + + // During the initial creation of the RenderWidgetHostView in + // TabContents::CreateRenderViewForRenderManager, SetSize is called with an + // empty size. In the Windows code flow, it is not ignored because subsequent + // sizing calls from the OS flow through TCVW::WasSized which calls SetSize() + // again. On Cocoa, we rely on the Cocoa view struture and resizer flags to + // keep things sized properly. On the other hand, if the size is not empty + // then this is a valid request for a pop-up. + if (rect.size().IsEmpty()) + return; + + // Ignore the position of |rect| for non-popup rwhvs. This is because + // background tabs do not have a window, but the window is required for the + // coordinate conversions. Popups are always for a visible tab. + if (IsPopup()) { + // The position of |rect| is screen coordinate system and we have to + // consider Cocoa coordinate system is upside-down and also multi-screen. + NSPoint origin_global = NSPointFromCGPoint(rect.origin().ToCGPoint()); + if ([[NSScreen screens] count] > 0) { + NSSize size = NSMakeSize(rect.width(), rect.height()); + size = [cocoa_view_ convertSize:size toView:nil]; + NSScreen* screen = + static_cast<NSScreen*>([[NSScreen screens] objectAtIndex:0]); + origin_global.y = + NSHeight([screen frame]) - size.height - origin_global.y; + } + + // Then |origin_global| is converted to client coordinate system. + DCHECK([cocoa_view_ window]); + NSPoint origin_window = + [[cocoa_view_ window] convertScreenToBase:origin_global]; + NSPoint origin_view = + [[cocoa_view_ superview] convertPoint:origin_window fromView:nil]; + NSRect frame = NSMakeRect(origin_view.x, origin_view.y, + rect.width(), rect.height()); + [cocoa_view_ setFrame:frame]; + } else { + DCHECK([[cocoa_view_ superview] isKindOfClass:[BaseView class]]); + BaseView* superview = static_cast<BaseView*>([cocoa_view_ superview]); + gfx::Rect rect = [superview flipNSRectToRect:[cocoa_view_ frame]]; + rect.set_width(rect.width()); + rect.set_height(rect.height()); + [cocoa_view_ setFrame:[superview flipRectToNSRect:rect]]; + } +} + +gfx::NativeView RenderWidgetHostViewMac::GetNativeView() const { + return native_view(); +} + +gfx::NativeViewId RenderWidgetHostViewMac::GetNativeViewId() const { + return reinterpret_cast<gfx::NativeViewId>(native_view()); +} + +void RenderWidgetHostViewMac::MovePluginWindows( + const std::vector<webkit::npapi::WebPluginGeometry>& moves) { + TRACE_EVENT0("browser", "RenderWidgetHostViewMac::MovePluginWindows"); + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + // Handle movement of accelerated plugins, which are the only "windowed" + // plugins that exist on the Mac. + for (std::vector<webkit::npapi::WebPluginGeometry>::const_iterator iter = + moves.begin(); + iter != moves.end(); + ++iter) { + webkit::npapi::WebPluginGeometry geom = *iter; + + AcceleratedPluginView* view = ViewForPluginWindowHandle(geom.window); + DCHECK(view); + if (!view) + continue; + + if (geom.rects_valid) { + gfx::Rect rect = geom.window_rect; + if (geom.visible) { + rect.set_x(rect.x() + geom.clip_rect.x()); + rect.set_y(rect.y() + geom.clip_rect.y()); + rect.set_width(geom.clip_rect.width()); + rect.set_height(geom.clip_rect.height()); + } + NSRect new_rect([cocoa_view_ flipRectToNSRect:rect]); + [view setFrame:new_rect]; + NSMutableArray* cutout_rects = + [NSMutableArray arrayWithCapacity:geom.cutout_rects.size()]; + for (unsigned int i = 0; i < geom.cutout_rects.size(); ++i) { + // Convert to NSRect, and flip vertically. + NSRect cutout_rect = NSRectFromCGRect(geom.cutout_rects[i].ToCGRect()); + cutout_rect.origin.y = new_rect.size.height - NSMaxY(cutout_rect); + [cutout_rects addObject:[NSValue valueWithRect:cutout_rect]]; + } + [view setCutoutRects:cutout_rects]; + [view setNeedsDisplay:YES]; + } + + plugin_container_manager_.SetPluginContainerGeometry(geom); + + BOOL visible = + plugin_container_manager_.SurfaceShouldBeVisible(geom.window); + [view setHidden:!visible]; + } +} + +void RenderWidgetHostViewMac::Focus() { + [[cocoa_view_ window] makeFirstResponder:cocoa_view_]; +} + +void RenderWidgetHostViewMac::Blur() { + [[cocoa_view_ window] makeFirstResponder:nil]; +} + +bool RenderWidgetHostViewMac::HasFocus() { + return [[cocoa_view_ window] firstResponder] == cocoa_view_; +} + +void RenderWidgetHostViewMac::Show() { + [cocoa_view_ setHidden:NO]; + + DidBecomeSelected(); +} + +void RenderWidgetHostViewMac::Hide() { + [cocoa_view_ setHidden:YES]; + + WasHidden(); +} + +bool RenderWidgetHostViewMac::IsShowing() { + return ![cocoa_view_ isHidden]; +} + +gfx::Rect RenderWidgetHostViewMac::GetViewBounds() const { + // TODO(shess): In case of !window, the view has been removed from + // the view hierarchy because the tab isn't main. Could retrieve + // the information from the main tab for our window. + NSWindow* enclosing_window = ApparentWindowForView(cocoa_view_); + if (!enclosing_window) + return gfx::Rect(); + + NSRect bounds = [cocoa_view_ bounds]; + bounds = [cocoa_view_ convertRect:bounds toView:nil]; + bounds.origin = [enclosing_window convertBaseToScreen:bounds.origin]; + return FlipNSRectToRectScreen(bounds); +} + +void RenderWidgetHostViewMac::UpdateCursor(const WebCursor& cursor) { + current_cursor_ = cursor; + UpdateCursorIfNecessary(); +} + +void RenderWidgetHostViewMac::UpdateCursorIfNecessary() { + // Do something special (as Win Chromium does) for arrow cursor while loading + // a page? TODO(avi): decide + + // Don't update the cursor if a context menu is being shown. + if (is_showing_context_menu_) + return; + + // Can we synchronize to the event stream? Switch to -[NSWindow + // mouseLocationOutsideOfEventStream] if we cannot. TODO(avi): test and see + NSEvent* event = [[cocoa_view_ window] currentEvent]; + if ([event window] != [cocoa_view_ window]) + return; + + NSCursor* ns_cursor = current_cursor_.GetCursor(); + [ns_cursor set]; +} + +void RenderWidgetHostViewMac::SetIsLoading(bool is_loading) { + is_loading_ = is_loading; + // If we ever decide to show the waiting cursor while the page is loading + // like Chrome does on Windows, call |UpdateCursorIfNecessary()| here. +} + +void RenderWidgetHostViewMac::ImeUpdateTextInputState( + ui::TextInputType type, + bool can_compose_inline, + const gfx::Rect& caret_rect) { + // TODO(kinaba): currently, can_compose_inline is ignored and always treated + // as true. We need to support "can_compose_inline=false" for PPAPI plugins + // that may want to avoid drawing composition-text by themselves and pass + // the responsibility to the browser. + if (text_input_type_ != type) { + text_input_type_ = type; + if (HasFocus()) { + SetTextInputActive(true); + + // Let AppKit cache the new input context to make IMEs happy. + // See http://crbug.com/73039. + [NSApp updateWindows]; + } + } +} + +void RenderWidgetHostViewMac::ImeCancelComposition() { + [cocoa_view_ cancelComposition]; +} + +void RenderWidgetHostViewMac::ImeCompositionRangeChanged( + const ui::Range& range) { + // The RangeChanged message is only sent with valid values. The current + // caret position (start == end) will be sent if there is no IME range. + [cocoa_view_ setMarkedRange:range.ToNSRange()]; +} + +void RenderWidgetHostViewMac::DidUpdateBackingStore( + const gfx::Rect& scroll_rect, int scroll_dx, int scroll_dy, + const std::vector<gfx::Rect>& copy_rects) { + if (!is_hidden_) { + std::vector<gfx::Rect> rects(copy_rects); + + // Because the findbar might be open, we cannot use scrollRect:by: here. For + // now, simply mark all of scroll rect as dirty. + if (!scroll_rect.IsEmpty()) + rects.push_back(scroll_rect); + + for (size_t i = 0; i < rects.size(); ++i) { + NSRect ns_rect = [cocoa_view_ flipRectToNSRect:rects[i]]; + + if (about_to_validate_and_paint_) { + // As much as we'd like to use -setNeedsDisplayInRect: here, we can't. + // We're in the middle of executing a -drawRect:, and as soon as it + // returns Cocoa will clear its record of what needs display. We + // instead use |performSelector:| to call |setNeedsDisplayInRect:| + // after returning to the main loop, at which point |drawRect:| is no + // longer on the stack. + DCHECK([NSThread isMainThread]); + if (!call_set_needs_display_in_rect_pending_) { + [cocoa_view_ performSelector:@selector(callSetNeedsDisplayInRect) + withObject:nil + afterDelay:0]; + call_set_needs_display_in_rect_pending_ = true; + invalid_rect_ = ns_rect; + } else { + // The old invalid rect is probably invalid now, since the view has + // most likely been resized, but there's no harm in dirtying the + // union. In the limit, this becomes equivalent to dirtying the + // whole view. + invalid_rect_ = NSUnionRect(invalid_rect_, ns_rect); + } + } else { + [cocoa_view_ setNeedsDisplayInRect:ns_rect]; + } + } + + if (!about_to_validate_and_paint_) + [cocoa_view_ displayIfNeeded]; + } + + // If |about_to_validate_and_paint_| is set, then -drawRect: is on the stack + // and it's not allowed to call -setHidden on the accelerated view. In that + // case, -callSetNeedsDisplayInRect: will hide it later. + // If |about_to_validate_and_paint_| is not set, do it now. + if (!about_to_validate_and_paint_) + HandleDelayedGpuViewHiding(); +} + +void RenderWidgetHostViewMac::RenderViewGone(base::TerminationStatus status, + int error_code) { + // TODO(darin): keep this around, and draw sad-tab into it. + Destroy(); +} + +void RenderWidgetHostViewMac::Destroy() { + // On Windows, popups are implemented with a popup window style, so that when + // an event comes in that would "cancel" it, it receives the OnCancelMode + // message and can kill itself. Alas, on the Mac, views cannot capture events + // outside of themselves. On Windows, if Destroy is being called on a view, + // then the event causing the destroy had also cancelled any popups by the + // time Destroy() was called. On the Mac we have to destroy all the popups + // ourselves. + + // Depth-first destroy all popups. Use ShutdownHost() to enforce + // deepest-first ordering. + for (NSView* subview in [cocoa_view_ subviews]) { + if ([subview isKindOfClass:[RenderWidgetHostViewCocoa class]]) { + [static_cast<RenderWidgetHostViewCocoa*>(subview) + renderWidgetHostViewMac]->ShutdownHost(); + } else if ([subview isKindOfClass:[AcceleratedPluginView class]]) { + [static_cast<AcceleratedPluginView*>(subview) + onRenderWidgetHostViewGone]; + } + } + + // We've been told to destroy. + [cocoa_view_ retain]; + [cocoa_view_ removeFromSuperview]; + [cocoa_view_ autorelease]; + + // We get this call just before |render_widget_host_| deletes + // itself. But we are owned by |cocoa_view_|, which may be retained + // by some other code. Examples are TabContentsViewMac's + // |latent_focus_view_| and TabWindowController's + // |cachedContentView_|. + render_widget_host_ = NULL; +} + +// Called from the renderer to tell us what the tooltip text should be. It +// calls us frequently so we need to cache the value to prevent doing a lot +// of repeat work. +void RenderWidgetHostViewMac::SetTooltipText(const string16& tooltip_text) { + if (tooltip_text != tooltip_text_ && [[cocoa_view_ window] isKeyWindow]) { + tooltip_text_ = tooltip_text; + + // Clamp the tooltip length to kMaxTooltipLength. It's a DOS issue on + // Windows; we're just trying to be polite. Don't persist the trimmed + // string, as then the comparison above will always fail and we'll try to + // set it again every single time the mouse moves. + string16 display_text = tooltip_text_; + if (tooltip_text_.length() > kMaxTooltipLength) + display_text = tooltip_text_.substr(0, kMaxTooltipLength); + + NSString* tooltip_nsstring = base::SysUTF16ToNSString(display_text); + [cocoa_view_ setToolTipAtMousePoint:tooltip_nsstring]; + } +} + +// +// RenderWidgetHostViewCocoa uses the stored selection text, +// which implements NSServicesRequests protocol. +// +void RenderWidgetHostViewMac::SelectionChanged(const std::string& text, + const ui::Range& range, + const gfx::Point& start, + const gfx::Point& end) { + selected_text_ = text; + [cocoa_view_ setSelectedRange:range.ToNSRange()]; + // Updaes markedRange when there is no marked text so that retrieving + // markedRange immediately after calling setMarkdText: returns the current + // caret position. + if (![cocoa_view_ hasMarkedText]) { + [cocoa_view_ setMarkedRange:range.ToNSRange()]; + } +} + +void RenderWidgetHostViewMac::ShowingContextMenu(bool showing) { + DCHECK_NE(is_showing_context_menu_, showing); + is_showing_context_menu_ = showing; + + // If the menu was closed, restore the cursor to the saved version initially, + // as the renderer will not re-send it if there was no change. + if (!showing) + UpdateCursorIfNecessary(); + + // Create a fake mouse event to inform the render widget that the mouse + // left or entered. + NSWindow* window = [cocoa_view_ window]; + // TODO(asvitkine): If the location outside of the event stream doesn't + // correspond to the current event (due to delayed event processing), then + // this may result in a cursor flicker if there are later mouse move events + // in the pipeline. Find a way to use the mouse location from the event that + // dismissed the context menu. + NSPoint location = [window mouseLocationOutsideOfEventStream]; + NSEvent* event = [NSEvent mouseEventWithType:NSMouseMoved + location:location + modifierFlags:0 + timestamp:0 + windowNumber:[window windowNumber] + context:nil + eventNumber:0 + clickCount:0 + pressure:0]; + WebMouseEvent web_event = + WebInputEventFactory::mouseEvent(event, cocoa_view_); + if (showing) + web_event.type = WebInputEvent::MouseLeave; + render_widget_host_->ForwardMouseEvent(web_event); +} + +bool RenderWidgetHostViewMac::IsPopup() const { + return popup_type_ != WebKit::WebPopupTypeNone; +} + +BackingStore* RenderWidgetHostViewMac::AllocBackingStore( + const gfx::Size& size) { + return new BackingStoreMac(render_widget_host_, size); +} + +// Sets whether or not to accept first responder status. +void RenderWidgetHostViewMac::SetTakesFocusOnlyOnMouseDown(bool flag) { + [cocoa_view_ setTakesFocusOnlyOnMouseDown:flag]; +} + +void RenderWidgetHostViewMac::KillSelf() { + if (shutdown_factory_.empty()) { + [cocoa_view_ setHidden:YES]; + MessageLoop::current()->PostTask(FROM_HERE, + shutdown_factory_.NewRunnableMethod( + &RenderWidgetHostViewMac::ShutdownHost)); + } +} + +void RenderWidgetHostViewMac::PluginFocusChanged(bool focused, int plugin_id) { + [cocoa_view_ pluginFocusChanged:(focused ? YES : NO) forPlugin:plugin_id]; +} + +void RenderWidgetHostViewMac::StartPluginIme() { + [cocoa_view_ setPluginImeActive:YES]; +} + +bool RenderWidgetHostViewMac::PostProcessEventForPluginIme( + const NativeWebKeyboardEvent& event) { + // Check WebInputEvent type since multiple types of events can be sent into + // WebKit for the same OS event (e.g., RawKeyDown and Char), so filtering is + // necessary to avoid double processing. + // Also check the native type, since NSFlagsChanged is considered a key event + // for WebKit purposes, but isn't considered a key event by the OS. + if (event.type == WebInputEvent::RawKeyDown && + [event.os_event type] == NSKeyDown) + return [cocoa_view_ postProcessEventForPluginIme:event.os_event]; + return false; +} + +void RenderWidgetHostViewMac::PluginImeCompositionCompleted( + const string16& text, int plugin_id) { + if (render_widget_host_) { + render_widget_host_->Send(new ViewMsg_PluginImeCompositionCompleted( + render_widget_host_->routing_id(), text, plugin_id)); + } +} + +gfx::PluginWindowHandle +RenderWidgetHostViewMac::AllocateFakePluginWindowHandle(bool opaque, + bool root) { + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + // |render_widget_host_| is set to NULL when |RWHVMac::Destroy()| has + // completed. If |AllocateFakePluginWindowHandle()| is called after that, + // we will crash when the AcceleratedPluginView we allocate below is + // destroyed. + DCHECK(render_widget_host_); + + // Create an NSView to host the plugin's/compositor's pixels. + gfx::PluginWindowHandle handle = + plugin_container_manager_.AllocateFakePluginWindowHandle(opaque, root); + + scoped_nsobject<AcceleratedPluginView> plugin_view( + [[AcceleratedPluginView alloc] initWithRenderWidgetHostViewMac:this + pluginHandle:handle]); + [plugin_view setHidden:YES]; + + [cocoa_view_ addSubview:plugin_view]; + plugin_views_[handle] = plugin_view; + + return handle; +} + +void RenderWidgetHostViewMac::DestroyFakePluginWindowHandle( + gfx::PluginWindowHandle window) { + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + PluginViewMap::iterator it = plugin_views_.find(window); + DCHECK(plugin_views_.end() != it); + if (plugin_views_.end() == it) { + return; + } + [it->second removeFromSuperview]; + plugin_views_.erase(it); + + // The view's dealloc will call DeallocFakePluginWindowHandle(), which will + // remove the handle from |plugin_container_manager_|. This code path is + // taken if a plugin is removed, but the RWHVMac itself stays alive. +} + +// This is called by AcceleratedPluginView's -dealloc. +void RenderWidgetHostViewMac::DeallocFakePluginWindowHandle( + gfx::PluginWindowHandle window) { + // When a browser window with a GpuScheduler is closed, the render process + // will attempt to finish all GL commands. It will busy-wait on the GPU + // process until the command queue is empty. If a paint is pending, the GPU + // process won't process any GL commands until the browser sends a paint ack, + // but since the browser window is already closed, it will never arrive. + // To resolve this we ask the GPU process to destroy the command buffer + // associated with the given render widget. Once the command buffer is + // destroyed, all GL commands from the renderer will immediately receive + // channel error. + if (render_widget_host_ && + plugin_container_manager_.IsRootContainer(window)) { + GpuProcessHost::SendOnIO( + render_widget_host_->process()->id(), + content::CAUSE_FOR_GPU_LAUNCH_NO_LAUNCH, + new GpuMsg_DestroyCommandBuffer( + render_widget_host_->process()->id(), + render_widget_host_->routing_id())); + } + + plugin_container_manager_.DestroyFakePluginWindowHandle(window); +} + +AcceleratedPluginView* RenderWidgetHostViewMac::ViewForPluginWindowHandle( + gfx::PluginWindowHandle window) { + PluginViewMap::iterator it = plugin_views_.find(window); + DCHECK(plugin_views_.end() != it); + if (plugin_views_.end() == it) + return nil; + return it->second; +} + +void RenderWidgetHostViewMac::AcceleratedSurfaceSetIOSurface( + gfx::PluginWindowHandle window, + int32 width, + int32 height, + uint64 io_surface_identifier) { + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + plugin_container_manager_.SetSizeAndIOSurface(window, + width, + height, + io_surface_identifier); + + if (plugin_container_manager_.IsRootContainer(window)) { + // Fake up a WebPluginGeometry for the root window to set the + // container's size; we will never get a notification from the + // browser about the root window, only plugins. + webkit::npapi::WebPluginGeometry geom; + gfx::Rect rect(0, 0, width, height); + geom.window = window; + geom.window_rect = rect; + geom.clip_rect = rect; + geom.visible = true; + geom.rects_valid = true; + MovePluginWindows(std::vector<webkit::npapi::WebPluginGeometry>(1, geom)); + } +} + +void RenderWidgetHostViewMac::AcceleratedSurfaceSetTransportDIB( + gfx::PluginWindowHandle window, + int32 width, + int32 height, + TransportDIB::Handle transport_dib) { + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + plugin_container_manager_.SetSizeAndTransportDIB(window, + width, + height, + transport_dib); +} + +void RenderWidgetHostViewMac::AcceleratedSurfaceBuffersSwapped( + gfx::PluginWindowHandle window, + uint64 surface_id, + int renderer_id, + int32 route_id, + int gpu_host_id, + uint64 swap_buffers_count) { + TRACE_EVENT1("browser", + "RenderWidgetHostViewMac::AcceleratedSurfaceBuffersSwapped", + "frameNum", swap_buffers_count); + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + AcceleratedPluginView* view = ViewForPluginWindowHandle(window); + DCHECK(view); + if (view) { + plugin_container_manager_.SetSurfaceWasPaintedTo(window, surface_id); + + // The surface is hidden until its first paint, to not show gargabe. + if (plugin_container_manager_.SurfaceShouldBeVisible(window)) + [view setHidden:NO]; + [view drawView]; + } + + if (renderer_id != 0 || route_id != 0) { + AcknowledgeSwapBuffers(renderer_id, + route_id, + gpu_host_id, + swap_buffers_count); + } +} + +void RenderWidgetHostViewMac::UpdateRootGpuViewVisibility( + bool show_gpu_widget) { + // Plugins are destroyed on page navigate. The compositor layer on the other + // hand is created on demand and then stays alive until its renderer process + // dies (usually on cross-domain navigation). Instead, only a flag + // |is_accelerated_compositing_active()| is flipped when the compositor output + // should be shown/hidden. + // Show/hide the view belonging to the compositor here. + plugin_container_manager_.set_gpu_rendering_active(show_gpu_widget); + + gfx::PluginWindowHandle root_handle = + plugin_container_manager_.root_container_handle(); + if (root_handle != gfx::kNullPluginWindow) { + AcceleratedPluginView* view = ViewForPluginWindowHandle(root_handle); + DCHECK(view); + bool visible = + plugin_container_manager_.SurfaceShouldBeVisible(root_handle); + [[view window] disableScreenUpdatesUntilFlush]; + [view setHidden:!visible]; + } +} + +void RenderWidgetHostViewMac::HandleDelayedGpuViewHiding() { + if (needs_gpu_visibility_update_after_repaint_) { + UpdateRootGpuViewVisibility(false); + needs_gpu_visibility_update_after_repaint_ = false; + } +} + +void RenderWidgetHostViewMac::AcknowledgeSwapBuffers( + int renderer_id, + int32 route_id, + int gpu_host_id, + uint64 swap_buffers_count) { + TRACE_EVENT1("gpu", "RenderWidgetHostViewMac::AcknowledgeSwapBuffers", + "swap_buffers_count", swap_buffers_count); + // Called on the display link thread. Hand actual work off to the IO thread, + // because |GpuProcessHost::Get()| can only be called there. + // Currently, this is never called for plugins. + if (render_widget_host_) { + DCHECK_EQ(render_widget_host_->process()->id(), renderer_id); + // |render_widget_host_->routing_id()| and |route_id| are usually not + // equal: The former identifies the channel from the RWH in the browser + // process to the corresponding render widget in the renderer process, while + // the latter identifies the channel from the GpuCommandBufferStub in the + // GPU process to the corresponding command buffer client in the renderer. + } + + // TODO(apatrick): Send the acknowledgement via the UI thread when running in + // single process or in process GPU mode for now. This is bad from a + // performance point of view but the plan is to not use AcceleratedSurface at + // all in these cases. + if (gpu_host_id == 0) { + BrowserThread::PostTask( + BrowserThread::UI, + FROM_HERE, + NewRunnableFunction(&GpuProcessHostUIShim::SendToGpuHost, + gpu_host_id, + new GpuMsg_AcceleratedSurfaceBuffersSwappedACK( + renderer_id, + route_id, + swap_buffers_count))); + } else { + GpuProcessHost::SendOnIO( + gpu_host_id, + content::CAUSE_FOR_GPU_LAUNCH_NO_LAUNCH, + new GpuMsg_AcceleratedSurfaceBuffersSwappedACK( + renderer_id, route_id, swap_buffers_count)); + } +} + +void RenderWidgetHostViewMac::GpuRenderingStateDidChange() { + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + if (GetRenderWidgetHost()->is_accelerated_compositing_active()) { + UpdateRootGpuViewVisibility( + GetRenderWidgetHost()->is_accelerated_compositing_active()); + } else { + needs_gpu_visibility_update_after_repaint_ = true; + } +} + +void RenderWidgetHostViewMac::GetScreenInfo(WebKit::WebScreenInfo* results) { + *results = WebKit::WebScreenInfoFactory::screenInfo(GetNativeView()); +} + +gfx::Rect RenderWidgetHostViewMac::GetRootWindowBounds() { + // TODO(shess): In case of !window, the view has been removed from + // the view hierarchy because the tab isn't main. Could retrieve + // the information from the main tab for our window. + NSWindow* enclosing_window = ApparentWindowForView(cocoa_view_); + if (!enclosing_window) + return gfx::Rect(); + + NSRect bounds = [enclosing_window frame]; + return FlipNSRectToRectScreen(bounds); +} + +gfx::PluginWindowHandle RenderWidgetHostViewMac::GetCompositingSurface() { + if (compositing_surface_ == gfx::kNullPluginWindow) + compositing_surface_ = AllocateFakePluginWindowHandle( + /*opaque=*/true, /*root=*/true); + return compositing_surface_; +} + +void RenderWidgetHostViewMac::DrawAcceleratedSurfaceInstance( + CGLContextObj context, + gfx::PluginWindowHandle plugin_handle, + NSSize size) { + TRACE_EVENT0("browser", + "RenderWidgetHostViewMac::DrawAcceleratedSurfaceInstance"); + // Called on the display link thread. + CGLSetCurrentContext(context); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + // Note that we place the origin at the upper left corner with +y + // going down + glOrtho(0, size.width, size.height, 0, -1, 1); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + plugin_container_manager_.Draw(context, plugin_handle); +} + +void RenderWidgetHostViewMac::ForceTextureReload() { + plugin_container_manager_.ForceTextureReload(); +} + +void RenderWidgetHostViewMac::SetVisuallyDeemphasized(const SkColor* color, + bool animate) { + // This is not used on mac. +} + +void RenderWidgetHostViewMac::UnhandledWheelEvent( + const WebKit::WebMouseWheelEvent& event) { + [cocoa_view_ gotUnhandledWheelEvent]; +} + +void RenderWidgetHostViewMac::SetHasHorizontalScrollbar( + bool has_horizontal_scrollbar) { + [cocoa_view_ setHasHorizontalScrollbar:has_horizontal_scrollbar]; +} + +void RenderWidgetHostViewMac::SetScrollOffsetPinning( + bool is_pinned_to_left, bool is_pinned_to_right) { + [cocoa_view_ scrollOffsetPinnedToLeft:is_pinned_to_left + toRight:is_pinned_to_right]; +} + +bool RenderWidgetHostViewMac::LockMouse() { + NOTIMPLEMENTED(); + return false; +} + +void RenderWidgetHostViewMac::UnlockMouse() { + NOTIMPLEMENTED(); +} + +void RenderWidgetHostViewMac::ShutdownHost() { + shutdown_factory_.RevokeAll(); + render_widget_host_->Shutdown(); + // Do not touch any members at this point, |this| has been deleted. +} + +gfx::Rect RenderWidgetHostViewMac::GetViewCocoaBounds() const { + return gfx::Rect(NSRectToCGRect([cocoa_view_ bounds])); +} + +void RenderWidgetHostViewMac::SetActive(bool active) { + if (render_widget_host_) { + render_widget_host_->Send(new ViewMsg_SetActive( + render_widget_host_->routing_id(), active)); + } + if (HasFocus()) + SetTextInputActive(active); + if (!active) + [cocoa_view_ setPluginImeActive:NO]; +} + +void RenderWidgetHostViewMac::SetWindowVisibility(bool visible) { + if (render_widget_host_) { + render_widget_host_->Send(new ViewMsg_SetWindowVisibility( + render_widget_host_->routing_id(), visible)); + } +} + +void RenderWidgetHostViewMac::WindowFrameChanged() { + if (render_widget_host_) { + render_widget_host_->Send(new ViewMsg_WindowFrameChanged( + render_widget_host_->routing_id(), GetRootWindowBounds(), + GetViewBounds())); + } +} + +void RenderWidgetHostViewMac::SetBackground(const SkBitmap& background) { + RenderWidgetHostView::SetBackground(background); + if (render_widget_host_) + render_widget_host_->Send(new ViewMsg_SetBackground( + render_widget_host_->routing_id(), background)); +} + +void RenderWidgetHostViewMac::OnAccessibilityNotifications( + const std::vector<ViewHostMsg_AccessibilityNotification_Params>& params) { + if (!browser_accessibility_manager_.get()) { + browser_accessibility_manager_.reset( + BrowserAccessibilityManager::CreateEmptyDocument( + cocoa_view_, static_cast<WebAccessibility::State>(0), NULL)); + } + browser_accessibility_manager_->OnAccessibilityNotifications(params); +} + +void RenderWidgetHostViewMac::SetTextInputActive(bool active) { + if (active) { + if (text_input_type_ == ui::TEXT_INPUT_TYPE_PASSWORD) + EnablePasswordInput(); + else + DisablePasswordInput(); + } else { + if (text_input_type_ == ui::TEXT_INPUT_TYPE_PASSWORD) + DisablePasswordInput(); + } +} + +// RenderWidgetHostViewCocoa --------------------------------------------------- + +@implementation RenderWidgetHostViewCocoa + +@synthesize selectedRange = selectedRange_; +@synthesize markedRange = markedRange_; + +- (id)initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)r { + self = [super initWithFrame:NSZeroRect]; + if (self) { + editCommand_helper_.reset(new RenderWidgetHostViewMacEditCommandHelper); + editCommand_helper_->AddEditingSelectorsToClass([self class]); + + renderWidgetHostView_.reset(r); + canBeKeyView_ = YES; + focusedPluginIdentifier_ = -1; + } + return self; +} + +- (void)dealloc { + if (delegate_ && [delegate_ respondsToSelector:@selector(viewGone:)]) + [delegate_ viewGone:self]; + + [super dealloc]; +} + +- (void)setRWHVDelegate:(RenderWidgetHostViewMacDelegate*)delegate { + delegate_ = delegate; +} + +- (void)gotUnhandledWheelEvent { + if (delegate_ && + [delegate_ respondsToSelector:@selector(gotUnhandledWheelEvent)]) { + [delegate_ gotUnhandledWheelEvent]; + } +} + +- (void)scrollOffsetPinnedToLeft:(BOOL)left toRight:(BOOL)right { + if (delegate_ && [delegate_ respondsToSelector: + @selector(scrollOffsetPinnedToLeft:toRight:)]) { + [delegate_ scrollOffsetPinnedToLeft:left toRight:right]; + } +} + +- (void)setHasHorizontalScrollbar:(BOOL)has_horizontal_scrollbar { + if (delegate_ && + [delegate_ respondsToSelector:@selector(setHasHorizontalScrollbar:)]) { + [delegate_ setHasHorizontalScrollbar:has_horizontal_scrollbar]; + } +} + +- (BOOL)respondsToSelector:(SEL)selector { + // Trickiness: this doesn't mean "does this object's superclass respond to + // this selector" but rather "does the -respondsToSelector impl from the + // superclass say that this class responds to the selector". + if ([super respondsToSelector:selector]) + return YES; + + if (delegate_) + return [delegate_ respondsToSelector:selector]; + else + return NO; +} + +- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + // Trickiness: this doesn't mean "does this object's superclass respond to + // this selector" but rather "does the -respondsToSelector impl from the + // superclass say that this class responds to the selector". + if ([super respondsToSelector:selector]) + return [super methodSignatureForSelector:selector]; + + if (delegate_) + return [delegate_ methodSignatureForSelector:selector]; + else + return nil; +} + +- (void)forwardInvocation:(NSInvocation*)invocation { + // TODO(avi): Oh, man, use -forwardingTargetForSelector: when 10.6 is the + // minimum requirement. + + if (delegate_ && [delegate_ respondsToSelector:[invocation selector]]) + [invocation invokeWithTarget:delegate_]; + else + [super forwardInvocation:invocation]; +} + +- (void)setCanBeKeyView:(BOOL)can { + canBeKeyView_ = can; +} + +- (void)setTakesFocusOnlyOnMouseDown:(BOOL)b { + takesFocusOnlyOnMouseDown_ = b; +} + +- (void)setCloseOnDeactivate:(BOOL)b { + closeOnDeactivate_ = b; +} + +- (BOOL)shouldIgnoreMouseEvent:(NSEvent*)theEvent { + NSWindow* window = [self window]; + // If this is a background window, don't handle mouse movement events. This + // is the expected behavior on the Mac as evidenced by other applications. + // Do this only if the window level is NSNormalWindowLevel, as this + // does not necessarily apply in other contexts (e.g. balloons). + if ([theEvent type] == NSMouseMoved && + [window level] == NSNormalWindowLevel && ![window isKeyWindow]) { + return YES; + } + + // Use hitTest to check whether the mouse is over a nonWebContentView - in + // which case the mouse event should not be handled by the render host. + const SEL nonWebContentViewSelector = @selector(nonWebContentView); + NSView* contentView = [window contentView]; + NSView* view = [contentView hitTest:[theEvent locationInWindow]]; + // Traverse the superview hierarchy as the hitTest will return the frontmost + // view, such as an NSTextView, while nonWebContentView may be specified by + // its parent view. + while (view) { + if ([view respondsToSelector:nonWebContentViewSelector] && + [view performSelector:nonWebContentViewSelector]) { + // The cursor is over a nonWebContentView - ignore this mouse event. + return YES; + } + view = [view superview]; + } + return NO; +} + +- (void)mouseEvent:(NSEvent*)theEvent { + if (delegate_ && [delegate_ respondsToSelector:@selector(handleEvent:)]) { + BOOL handled = [delegate_ handleEvent:theEvent]; + if (handled) + return; + } + + if ([self shouldIgnoreMouseEvent:theEvent]) { + // If this is the first such event, send a mouse exit to the host view. + if (!mouseEventWasIgnored_ && renderWidgetHostView_->render_widget_host_) { + WebMouseEvent exitEvent = + WebInputEventFactory::mouseEvent(theEvent, self); + exitEvent.type = WebInputEvent::MouseLeave; + exitEvent.button = WebMouseEvent::ButtonNone; + renderWidgetHostView_->render_widget_host_->ForwardMouseEvent(exitEvent); + } + mouseEventWasIgnored_ = YES; + return; + } + + if (mouseEventWasIgnored_) { + // If this is the first mouse event after a previous event that was ignored + // due to the hitTest, send a mouse enter event to the host view. + if (renderWidgetHostView_->render_widget_host_) { + WebMouseEvent enterEvent = + WebInputEventFactory::mouseEvent(theEvent, self); + enterEvent.type = WebInputEvent::MouseMove; + enterEvent.button = WebMouseEvent::ButtonNone; + renderWidgetHostView_->render_widget_host_->ForwardMouseEvent(enterEvent); + } + } + mouseEventWasIgnored_ = NO; + + // TODO(rohitrao): Probably need to handle other mouse down events here. + if ([theEvent type] == NSLeftMouseDown && takesFocusOnlyOnMouseDown_) { + if (renderWidgetHostView_->render_widget_host_) + renderWidgetHostView_->render_widget_host_->OnMouseActivate(); + + // Manually take focus after the click but before forwarding it to the + // renderer. + [[self window] makeFirstResponder:self]; + } + + // Don't cancel child popups; killing them on a mouse click would prevent the + // user from positioning the insertion point in the text field spawning the + // popup. A click outside the text field would cause the text field to drop + // the focus, and then EditorClientImpl::textFieldDidEndEditing() would cancel + // the popup anyway, so we're OK. + + NSEventType type = [theEvent type]; + if (type == NSLeftMouseDown) + hasOpenMouseDown_ = YES; + else if (type == NSLeftMouseUp) + hasOpenMouseDown_ = NO; + + // TODO(suzhe): We should send mouse events to the input method first if it + // wants to handle them. But it won't work without implementing method + // - (NSUInteger)characterIndexForPoint:. + // See: http://code.google.com/p/chromium/issues/detail?id=47141 + // Instead of sending mouse events to the input method first, we now just + // simply confirm all ongoing composition here. + if (type == NSLeftMouseDown || type == NSRightMouseDown || + type == NSOtherMouseDown) { + [self confirmComposition]; + } + + const WebMouseEvent& event = + WebInputEventFactory::mouseEvent(theEvent, self); + + if (renderWidgetHostView_->render_widget_host_) + renderWidgetHostView_->render_widget_host_->ForwardMouseEvent(event); +} + +- (void)shortCircuitEndGestureWithEvent:(NSEvent*)event { + DCHECK(base::mac::IsOSLionOrLater()); + + if ([event subtype] != kIOHIDEventTypeScroll) + return; + + if (renderWidgetHostView_->render_widget_host_) { + WebGestureEvent webEvent = WebInputEventFactory::gestureEvent(event, self); + renderWidgetHostView_->render_widget_host_->ForwardGestureEvent(webEvent); + } + + if (endGestureMonitor_) { + [NSEvent removeMonitor:endGestureMonitor_]; + endGestureMonitor_ = nil; + } +} + +- (void)beginGestureWithEvent:(NSEvent*)event { + if (base::mac::IsOSLionOrLater() && + [event subtype] == kIOHIDEventTypeScroll && + renderWidgetHostView_->render_widget_host_) { + WebGestureEvent webEvent = WebInputEventFactory::gestureEvent(event, self); + renderWidgetHostView_->render_widget_host_->ForwardGestureEvent(webEvent); + + // Use an NSEvent monitor to get the gesture-end event. This is done in + // order to get the gesture-end, even if the view is not visible, which is + // not the case with -endGestureWithEvent:. An example scenario where this + // may happen is switching tabs while a gesture is in progress. + if (!endGestureMonitor_) { + endGestureMonitor_ = + [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskEndGesture + handler:^(NSEvent* blockEvent) { + [self shortCircuitEndGestureWithEvent:blockEvent]; + return blockEvent; + }]; + } + } +} + +- (BOOL)performKeyEquivalent:(NSEvent*)theEvent { + // |performKeyEquivalent:| is sent to all views of a window, not only down the + // responder chain (cf. "Handling Key Equivalents" in + // http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/EventOverview/HandlingKeyEvents/HandlingKeyEvents.html + // ). We only want to handle key equivalents if we're first responder. + if ([[self window] firstResponder] != self) + return NO; + + // If we return |NO| from this function, cocoa will send the key event to + // the menu and only if the menu does not process the event to |keyDown:|. We + // want to send the event to a renderer _before_ sending it to the menu, so + // we need to return |YES| for all events that might be swallowed by the menu. + // We do not return |YES| for every keypress because we don't get |keyDown:| + // events for keys that we handle this way. + NSUInteger modifierFlags = [theEvent modifierFlags]; + if ((modifierFlags & NSCommandKeyMask) == 0) { + // Make sure the menu does not contain key equivalents that don't + // contain cmd. + DCHECK(![[NSApp mainMenu] performKeyEquivalent:theEvent]); + return NO; + } + + // Command key combinations are sent via performKeyEquivalent rather than + // keyDown:. We just forward this on and if WebCore doesn't want to handle + // it, we let the TabContentsView figure out how to reinject it. + [self keyEvent:theEvent wasKeyEquivalent:YES]; + return YES; +} + +- (BOOL)_wantsKeyDownForEvent:(NSEvent*)event { + // This is a SPI that AppKit apparently calls after |performKeyEquivalent:| + // returned NO. If this function returns |YES|, Cocoa sends the event to + // |keyDown:| instead of doing other things with it. Ctrl-tab will be sent + // to us instead of doing key view loop control, ctrl-left/right get handled + // correctly, etc. + // (However, there are still some keys that Cocoa swallows, e.g. the key + // equivalent that Cocoa uses for toggling the input language. In this case, + // that's actually a good thing, though -- see http://crbug.com/26115 .) + return YES; +} + +- (void)keyEvent:(NSEvent*)theEvent { + if (delegate_ && [delegate_ respondsToSelector:@selector(handleEvent:)]) { + BOOL handled = [delegate_ handleEvent:theEvent]; + if (handled) + return; + } + + [self keyEvent:theEvent wasKeyEquivalent:NO]; +} + +- (void)keyEvent:(NSEvent*)theEvent wasKeyEquivalent:(BOOL)equiv { + DCHECK([theEvent type] != NSKeyDown || + !equiv == !([theEvent modifierFlags] & NSCommandKeyMask)); + + if ([theEvent type] == NSFlagsChanged) { + // Ignore NSFlagsChanged events from the NumLock and Fn keys as + // Safari does in -[WebHTMLView flagsChanged:] (of "WebHTMLView.mm"). + int keyCode = [theEvent keyCode]; + if (!keyCode || keyCode == 10 || keyCode == 63) + return; + } + + // Don't cancel child popups; the key events are probably what's triggering + // the popup in the first place. + + RenderWidgetHost* widgetHost = renderWidgetHostView_->render_widget_host_; + DCHECK(widgetHost); + + NativeWebKeyboardEvent event(theEvent); + + // We only handle key down events and just simply forward other events. + if ([theEvent type] != NSKeyDown) { + widgetHost->ForwardKeyboardEvent(event); + + // Possibly autohide the cursor. + if ([RenderWidgetHostViewCocoa shouldAutohideCursorForEvent:theEvent]) + [NSCursor setHiddenUntilMouseMoves:YES]; + + return; + } + + scoped_nsobject<RenderWidgetHostViewCocoa> keepSelfAlive([self retain]); + + // Records the current marked text state, so that we can know if the marked + // text was deleted or not after handling the key down event. + BOOL oldHasMarkedText = hasMarkedText_; + + // This method should not be called recursively. + DCHECK(!handlingKeyDown_); + + // Tells insertText: and doCommandBySelector: that we are handling a key + // down event. + handlingKeyDown_ = YES; + + // These variables might be set when handling the keyboard event. + // Clear them here so that we can know whether they have changed afterwards. + textToBeInserted_.clear(); + markedText_.clear(); + underlines_.clear(); + unmarkTextCalled_ = NO; + hasEditCommands_ = NO; + editCommands_.clear(); + + // Before doing anything with a key down, check to see if plugin IME has been + // cancelled, since the plugin host needs to be informed of that before + // receiving the keydown. + if ([theEvent type] == NSKeyDown) + [self checkForPluginImeCancellation]; + + // Sends key down events to input method first, then we can decide what should + // be done according to input method's feedback. + // If a plugin is active, bypass this step since events are forwarded directly + // to the plugin IME. + if (focusedPluginIdentifier_ == -1) + [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]]; + + handlingKeyDown_ = NO; + + // Indicates if we should send the key event and corresponding editor commands + // after processing the input method result. + BOOL delayEventUntilAfterImeCompostion = NO; + + // To emulate Windows, over-write |event.windowsKeyCode| to VK_PROCESSKEY + // while an input method is composing or inserting a text. + // Gmail checks this code in its onkeydown handler to stop auto-completing + // e-mail addresses while composing a CJK text. + // If the text to be inserted has only one character, then we don't need this + // trick, because we'll send the text as a key press event instead. + if (hasMarkedText_ || oldHasMarkedText || textToBeInserted_.length() > 1) { + NativeWebKeyboardEvent fakeEvent = event; + fakeEvent.windowsKeyCode = 0xE5; // VKEY_PROCESSKEY + fakeEvent.setKeyIdentifierFromWindowsKeyCode(); + fakeEvent.skip_in_browser = true; + widgetHost->ForwardKeyboardEvent(fakeEvent); + // If this key event was handled by the input method, but + // -doCommandBySelector: (invoked by the call to -interpretKeyEvents: above) + // enqueued edit commands, then in order to let webkit handle them + // correctly, we need to send the real key event and corresponding edit + // commands after processing the input method result. + // We shouldn't do this if a new marked text was set by the input method, + // otherwise the new marked text might be cancelled by webkit. + if (hasEditCommands_ && !hasMarkedText_) + delayEventUntilAfterImeCompostion = YES; + } else { + if (!editCommands_.empty()) { + widgetHost->Send(new ViewMsg_SetEditCommandsForNextKeyEvent( + widgetHost->routing_id(), editCommands_)); + } + widgetHost->ForwardKeyboardEvent(event); + } + + // Calling ForwardKeyboardEvent() could have destroyed the widget. When the + // widget was destroyed, |renderWidgetHostView_->render_widget_host_| will + // be set to NULL. So we check it here and return immediately if it's NULL. + if (!renderWidgetHostView_->render_widget_host_) + return; + + // Then send keypress and/or composition related events. + // If there was a marked text or the text to be inserted is longer than 1 + // character, then we send the text by calling ConfirmComposition(). + // Otherwise, if the text to be inserted only contains 1 character, then we + // can just send a keypress event which is fabricated by changing the type of + // the keydown event, so that we can retain all necessary informations, such + // as unmodifiedText, etc. And we need to set event.skip_in_browser to true to + // prevent the browser from handling it again. + // Note that, |textToBeInserted_| is a UTF-16 string, but it's fine to only + // handle BMP characters here, as we can always insert non-BMP characters as + // text. + BOOL textInserted = NO; + if (textToBeInserted_.length() > + ((hasMarkedText_ || oldHasMarkedText) ? 0u : 1u)) { + widgetHost->ImeConfirmComposition(textToBeInserted_); + textInserted = YES; + } + + // Updates or cancels the composition. If some text has been inserted, then + // we don't need to cancel the composition explicitly. + if (hasMarkedText_ && markedText_.length()) { + // Sends the updated marked text to the renderer so it can update the + // composition node in WebKit. + // When marked text is available, |selectedRange_| will be the range being + // selected inside the marked text. + widgetHost->ImeSetComposition(markedText_, underlines_, + selectedRange_.location, + NSMaxRange(selectedRange_)); + } else if (oldHasMarkedText && !hasMarkedText_ && !textInserted) { + if (unmarkTextCalled_) + widgetHost->ImeConfirmComposition(); + else + widgetHost->ImeCancelComposition(); + } + + // If the key event was handled by the input method but it also generated some + // edit commands, then we need to send the real key event and corresponding + // edit commands here. This usually occurs when the input method wants to + // finish current composition session but still wants the application to + // handle the key event. See http://crbug.com/48161 for reference. + if (delayEventUntilAfterImeCompostion) { + // If |delayEventUntilAfterImeCompostion| is YES, then a fake key down event + // with windowsKeyCode == 0xE5 has already been sent to webkit. + // So before sending the real key down event, we need to send a fake key up + // event to balance it. + NativeWebKeyboardEvent fakeEvent = event; + fakeEvent.type = WebKit::WebInputEvent::KeyUp; + fakeEvent.skip_in_browser = true; + widgetHost->ForwardKeyboardEvent(fakeEvent); + // Not checking |renderWidgetHostView_->render_widget_host_| here because + // a key event with |skip_in_browser| == true won't be handled by browser, + // thus it won't destroy the widget. + + if (!editCommands_.empty()) { + widgetHost->Send(new ViewMsg_SetEditCommandsForNextKeyEvent( + widgetHost->routing_id(), editCommands_)); + } + widgetHost->ForwardKeyboardEvent(event); + + // Calling ForwardKeyboardEvent() could have destroyed the widget. When the + // widget was destroyed, |renderWidgetHostView_->render_widget_host_| will + // be set to NULL. So we check it here and return immediately if it's NULL. + if (!renderWidgetHostView_->render_widget_host_) + return; + } + + const NSUInteger kCtrlCmdKeyMask = NSControlKeyMask | NSCommandKeyMask; + // Only send a corresponding key press event if there is no marked text. + if (!hasMarkedText_) { + if (!textInserted && textToBeInserted_.length() == 1) { + // If a single character was inserted, then we just send it as a keypress + // event. + event.type = WebKit::WebInputEvent::Char; + event.text[0] = textToBeInserted_[0]; + event.text[1] = 0; + event.skip_in_browser = true; + widgetHost->ForwardKeyboardEvent(event); + } else if ((!textInserted || delayEventUntilAfterImeCompostion) && + [[theEvent characters] length] > 0 && + (([theEvent modifierFlags] & kCtrlCmdKeyMask) || + (hasEditCommands_ && editCommands_.empty()))) { + // We don't get insertText: calls if ctrl or cmd is down, or the key event + // generates an insert command. So synthesize a keypress event for these + // cases, unless the key event generated any other command. + event.type = WebKit::WebInputEvent::Char; + event.skip_in_browser = true; + widgetHost->ForwardKeyboardEvent(event); + } + } + + // Possibly autohide the cursor. + if ([RenderWidgetHostViewCocoa shouldAutohideCursorForEvent:theEvent]) + [NSCursor setHiddenUntilMouseMoves:YES]; +} + +- (void)scrollWheel:(NSEvent*)theEvent { + // Cancel popups before calling the delegate because even if the delegate eats + // the event, it's still an explicit user action outside the popup. + [self cancelChildPopups]; + + if (delegate_ && [delegate_ respondsToSelector:@selector(handleEvent:)]) { + BOOL handled = [delegate_ handleEvent:theEvent]; + if (handled) + return; + } + + const WebMouseWheelEvent& event = + WebInputEventFactory::mouseWheelEvent(theEvent, self); + if (renderWidgetHostView_->render_widget_host_) + renderWidgetHostView_->render_widget_host_->ForwardWheelEvent(event); +} + +// See the comment in RenderWidgetHostViewMac::Destroy() about cancellation +// events. On the Mac we must kill popups on outside events, thus this lovely +// case of filicide caused by events on parent views. +- (void)cancelChildPopups { + // If this view can be the key view, it is not a popup. Therefore, if it has + // any children, they are popups that need to be canceled. + if (canBeKeyView_) { + for (NSView* subview in [self subviews]) { + if (![subview isKindOfClass:[RenderWidgetHostViewCocoa class]]) + continue; // Skip plugin views. + + [static_cast<RenderWidgetHostViewCocoa*>(subview) + renderWidgetHostViewMac]->KillSelf(); + } + } +} + +- (void)setFrameSize:(NSSize)newSize { + [super setFrameSize:newSize]; + if (renderWidgetHostView_->render_widget_host_) + renderWidgetHostView_->render_widget_host_->WasResized(); +} + +- (void)setFrame:(NSRect)frameRect { + [super setFrame:frameRect]; + if (renderWidgetHostView_->render_widget_host_) + renderWidgetHostView_->render_widget_host_->WasResized(); +} + +- (void)setFrameWithDeferredUpdate:(NSRect)frameRect { + [super setFrame:frameRect]; + [self performSelector:@selector(renderWidgetHostWasResized) + withObject:nil + afterDelay:0]; +} + +- (void)renderWidgetHostWasResized { + if (renderWidgetHostView_->render_widget_host_) + renderWidgetHostView_->render_widget_host_->WasResized(); +} + +- (void)callSetNeedsDisplayInRect { + DCHECK([NSThread isMainThread]); + DCHECK(renderWidgetHostView_->call_set_needs_display_in_rect_pending_); + [self setNeedsDisplayInRect:renderWidgetHostView_->invalid_rect_]; + renderWidgetHostView_->call_set_needs_display_in_rect_pending_ = false; + renderWidgetHostView_->invalid_rect_ = NSZeroRect; + + renderWidgetHostView_->HandleDelayedGpuViewHiding(); +} + +// Fills with white the parts of the area to the right and bottom for |rect| +// that intersect |damagedRect|. +- (void)fillBottomRightRemainderOfRect:(gfx::Rect)rect + dirtyRect:(gfx::Rect)damagedRect { + if (damagedRect.right() > rect.right()) { + int x = std::max(rect.right(), damagedRect.x()); + int y = std::min(rect.bottom(), damagedRect.bottom()); + int width = damagedRect.right() - x; + int height = damagedRect.y() - y; + + // Extra fun to get around the fact that gfx::Rects can't have + // negative sizes. + if (width < 0) { + x += width; + width = -width; + } + if (height < 0) { + y += height; + height = -height; + } + + NSRect r = [self flipRectToNSRect:gfx::Rect(x, y, width, height)]; + [[NSColor whiteColor] set]; + NSRectFill(r); + } + if (damagedRect.bottom() > rect.bottom()) { + int x = damagedRect.x(); + int y = damagedRect.bottom(); + int width = damagedRect.right() - x; + int height = std::max(rect.bottom(), damagedRect.y()) - y; + + // Extra fun to get around the fact that gfx::Rects can't have + // negative sizes. + if (width < 0) { + x += width; + width = -width; + } + if (height < 0) { + y += height; + height = -height; + } + + NSRect r = [self flipRectToNSRect:gfx::Rect(x, y, width, height)]; + [[NSColor whiteColor] set]; + NSRectFill(r); + } +} + +- (void)drawRect:(NSRect)dirtyRect { + if (!renderWidgetHostView_->render_widget_host_) { + // TODO(shess): Consider using something more noticable? + [[NSColor whiteColor] set]; + NSRectFill(dirtyRect); + return; + } + + const gfx::Rect damagedRect([self flipNSRectToRect:dirtyRect]); + + if (renderWidgetHostView_->render_widget_host_-> + is_accelerated_compositing_active()) { + gfx::Rect gpuRect; + + gfx::PluginWindowHandle root_handle = + renderWidgetHostView_->plugin_container_manager_.root_container_handle(); + if (root_handle != gfx::kNullPluginWindow) { + AcceleratedPluginView* view = + renderWidgetHostView_->ViewForPluginWindowHandle(root_handle); + DCHECK(view); + if (view && ![view isHidden]) { + gpuRect = [self flipNSRectToRect:[view frame]]; + } + } + + [self fillBottomRightRemainderOfRect:gpuRect dirtyRect:damagedRect]; + return; + } + + DCHECK(!renderWidgetHostView_->about_to_validate_and_paint_); + + renderWidgetHostView_->about_to_validate_and_paint_ = true; + BackingStoreMac* backingStore = static_cast<BackingStoreMac*>( + renderWidgetHostView_->render_widget_host_->GetBackingStore(true)); + renderWidgetHostView_->about_to_validate_and_paint_ = false; + + if (backingStore) { + gfx::Rect bitmapRect(0, 0, + backingStore->size().width(), + backingStore->size().height()); + + // Specify the proper y offset to ensure that the view is rooted to the + // upper left corner. This can be negative, if the window was resized + // smaller and the renderer hasn't yet repainted. + int yOffset = NSHeight([self bounds]) - backingStore->size().height(); + + gfx::Rect paintRect = bitmapRect.Intersect(damagedRect); + if (!paintRect.IsEmpty()) { + // if we have a CGLayer, draw that into the window + if (backingStore->cg_layer()) { + CGContextRef context = static_cast<CGContextRef>( + [[NSGraphicsContext currentContext] graphicsPort]); + + // TODO: add clipping to dirtyRect if it improves drawing performance. + CGContextDrawLayerAtPoint(context, CGPointMake(0.0, yOffset), + backingStore->cg_layer()); + } else { + // if we haven't created a layer yet, draw the cached bitmap into + // the window. The CGLayer will be created the next time the renderer + // paints. + CGContextRef context = static_cast<CGContextRef>( + [[NSGraphicsContext currentContext] graphicsPort]); + base::mac::ScopedCFTypeRef<CGImageRef> image( + CGBitmapContextCreateImage(backingStore->cg_bitmap())); + CGRect imageRect = bitmapRect.ToCGRect(); + imageRect.origin.y = yOffset; + CGContextDrawImage(context, imageRect, image); + } + } + + // Fill the remaining portion of the damagedRect with white + [self fillBottomRightRemainderOfRect:bitmapRect dirtyRect:damagedRect]; + + if (!renderWidgetHostView_->whiteout_start_time_.is_null()) { + base::TimeDelta whiteout_duration = base::TimeTicks::Now() - + renderWidgetHostView_->whiteout_start_time_; + UMA_HISTOGRAM_TIMES("MPArch.RWHH_WhiteoutDuration", whiteout_duration); + + // Reset the start time to 0 so that we start recording again the next + // time the backing store is NULL... + renderWidgetHostView_->whiteout_start_time_ = base::TimeTicks(); + } + if (!renderWidgetHostView_->tab_switch_paint_time_.is_null()) { + base::TimeDelta tab_switch_paint_duration = base::TimeTicks::Now() - + renderWidgetHostView_->tab_switch_paint_time_; + UMA_HISTOGRAM_TIMES("MPArch.RWH_TabSwitchPaintDuration", + tab_switch_paint_duration); + // Reset tab_switch_paint_time_ to 0 so future tab selections are + // recorded. + renderWidgetHostView_->tab_switch_paint_time_ = base::TimeTicks(); + } + } else { + [[NSColor whiteColor] set]; + NSRectFill(dirtyRect); + if (renderWidgetHostView_->whiteout_start_time_.is_null()) + renderWidgetHostView_->whiteout_start_time_ = base::TimeTicks::Now(); + } +} + +- (BOOL)canBecomeKeyView { + if (!renderWidgetHostView_->render_widget_host_) + return NO; + + return canBeKeyView_; +} + +- (BOOL)acceptsFirstResponder { + if (!renderWidgetHostView_->render_widget_host_) + return NO; + + return canBeKeyView_ && !takesFocusOnlyOnMouseDown_; +} + +- (BOOL)becomeFirstResponder { + if (!renderWidgetHostView_->render_widget_host_) + return NO; + + renderWidgetHostView_->render_widget_host_->Focus(); + renderWidgetHostView_->render_widget_host_->SetInputMethodActive(true); + renderWidgetHostView_->SetTextInputActive(true); + + // Cancel any onging composition text which was left before we lost focus. + // TODO(suzhe): We should do it in -resignFirstResponder: method, but + // somehow that method won't be called when switching among different tabs. + // See http://crbug.com/47209 + [self cancelComposition]; + + NSNumber* direction = [NSNumber numberWithUnsignedInteger: + [[self window] keyViewSelectionDirection]]; + NSDictionary* userInfo = + [NSDictionary dictionaryWithObject:direction + forKey:kSelectionDirection]; + [[NSNotificationCenter defaultCenter] + postNotificationName:kViewDidBecomeFirstResponder + object:self + userInfo:userInfo]; + + return YES; +} + +- (BOOL)resignFirstResponder { + renderWidgetHostView_->SetTextInputActive(false); + if (!renderWidgetHostView_->render_widget_host_) + return YES; + + if (closeOnDeactivate_) + renderWidgetHostView_->KillSelf(); + + renderWidgetHostView_->render_widget_host_->SetInputMethodActive(false); + renderWidgetHostView_->render_widget_host_->Blur(); + + // We should cancel any onging composition whenever RWH's Blur() method gets + // called, because in this case, webkit will confirm the ongoing composition + // internally. + [self cancelComposition]; + + return YES; +} + +- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item { + if (delegate_ && [delegate_ respondsToSelector: + @selector(validateUserInterfaceItem:isValidItem:)]) { + BOOL valid; + BOOL known = [delegate_ validateUserInterfaceItem:item + isValidItem:&valid]; + if (known) + return valid; + } + + SEL action = [item action]; + + // For now, these actions are always enabled for render view, + // this is sub-optimal. + // TODO(suzhe): Plumb the "can*" methods up from WebCore. + if (action == @selector(undo:) || + action == @selector(redo:) || + action == @selector(cut:) || + action == @selector(copy:) || + action == @selector(copyToFindPboard:) || + action == @selector(paste:) || + action == @selector(pasteAsPlainText:)) { + return renderWidgetHostView_->render_widget_host_->IsRenderView(); + } + + return editCommand_helper_->IsMenuItemEnabled(action, self); +} + +- (RenderWidgetHostViewMac*)renderWidgetHostViewMac { + return renderWidgetHostView_.get(); +} + +// Determine whether we should autohide the cursor (i.e., hide it until mouse +// move) for the given event. Customize here to be more selective about which +// key presses to autohide on. ++ (BOOL)shouldAutohideCursorForEvent:(NSEvent*)event { + return ([event type] == NSKeyDown) ? YES : NO; +} + +- (NSArray *)accessibilityArrayAttributeValues:(NSString *)attribute + index:(NSUInteger)index + maxCount:(NSUInteger)maxCount { + NSArray* fullArray = [self accessibilityAttributeValue:attribute]; + NSUInteger totalLength = [fullArray count]; + if (index >= totalLength) + return nil; + NSUInteger length = MIN(totalLength - index, maxCount); + return [fullArray subarrayWithRange:NSMakeRange(index, length)]; +} + +- (NSUInteger)accessibilityArrayAttributeCount:(NSString *)attribute { + NSArray* fullArray = [self accessibilityAttributeValue:attribute]; + return [fullArray count]; +} + +- (id)accessibilityAttributeValue:(NSString *)attribute { + BrowserAccessibilityManager* manager = + renderWidgetHostView_->browser_accessibility_manager_.get(); + + // Contents specifies document view of RenderWidgetHostViewCocoa provided by + // BrowserAccessibilityManager. Children includes all subviews in addition to + // contents. Currently we do not have subviews besides the document view. + if (([attribute isEqualToString:NSAccessibilityChildrenAttribute] || + [attribute isEqualToString:NSAccessibilityContentsAttribute]) && + manager) { + return [NSArray arrayWithObjects:manager-> + GetRoot()->toBrowserAccessibilityCocoa(), nil]; + } else if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) { + return NSAccessibilityScrollAreaRole; + } + id ret = [super accessibilityAttributeValue:attribute]; + return ret; +} + +- (NSArray*)accessibilityAttributeNames { + NSMutableArray* ret = [[[NSMutableArray alloc] init] autorelease]; + [ret addObject:NSAccessibilityContentsAttribute]; + [ret addObjectsFromArray:[super accessibilityAttributeNames]]; + return ret; +} + +- (id)accessibilityHitTest:(NSPoint)point { + if (!renderWidgetHostView_->browser_accessibility_manager_.get()) + return self; + NSPoint pointInWindow = [[self window] convertScreenToBase:point]; + NSPoint localPoint = [self convertPoint:pointInWindow fromView:nil]; + localPoint.y = NSHeight([self bounds]) - localPoint.y; + BrowserAccessibilityCocoa* root = renderWidgetHostView_-> + browser_accessibility_manager_-> + GetRoot()->toBrowserAccessibilityCocoa(); + id obj = [root accessibilityHitTest:localPoint]; + return obj; +} + +- (BOOL)accessibilityIsIgnored { + return NO; +} + +- (NSUInteger)accessibilityGetIndexOf:(id)child { + BrowserAccessibilityManager* manager = + renderWidgetHostView_->browser_accessibility_manager_.get(); + // Only child is root. + if (manager && + manager->GetRoot()->toBrowserAccessibilityCocoa() == child) { + return 0; + } else { + return NSNotFound; + } +} + +- (id)accessibilityFocusedUIElement { + BrowserAccessibilityManager* manager = + renderWidgetHostView_->browser_accessibility_manager_.get(); + if (manager) { + BrowserAccessibility* focused_item = manager->GetFocus(NULL); + DCHECK(focused_item); + if (focused_item) { + BrowserAccessibilityCocoa* focused_item_cocoa = + focused_item->toBrowserAccessibilityCocoa(); + DCHECK(focused_item_cocoa); + if (focused_item_cocoa) + return focused_item_cocoa; + } + } + return [super accessibilityFocusedUIElement]; +} + +- (void)doDefaultAction:(int32)accessibilityObjectId { + RenderWidgetHost* rwh = renderWidgetHostView_->render_widget_host_; + rwh->Send(new ViewMsg_AccessibilityDoDefaultAction( + rwh->routing_id(), accessibilityObjectId)); +} + +// Convert a web accessibility's location in web coordinates into a cocoa +// screen coordinate. +- (NSPoint)accessibilityPointInScreen: + (BrowserAccessibilityCocoa*)accessibility { + NSPoint origin = [accessibility origin]; + NSSize size = [[accessibility size] sizeValue]; + origin.y = NSHeight([self bounds]) - origin.y; + NSPoint originInWindow = [self convertPoint:origin toView:nil]; + NSPoint originInScreen = [[self window] convertBaseToScreen:originInWindow]; + originInScreen.y = originInScreen.y - size.height; + return originInScreen; +} + +- (void)setAccessibilityFocus:(BOOL)focus + accessibilityId:(int32)accessibilityObjectId { + if (focus) { + RenderWidgetHost* rwh = renderWidgetHostView_->render_widget_host_; + rwh->Send(new ViewMsg_SetAccessibilityFocus( + rwh->routing_id(), accessibilityObjectId)); + } +} + +- (void)performShowMenuAction:(BrowserAccessibilityCocoa*)accessibility { + // Performs a right click copying WebKit's + // accessibilityPerformShowMenuAction. + NSPoint location = [self accessibilityPointInScreen:accessibility]; + NSSize size = [[accessibility size] sizeValue]; + location = [[self window] convertScreenToBase:location]; + location.x += size.width/2; + location.y += size.height/2; + + NSEvent* fakeRightClick = [NSEvent + mouseEventWithType:NSRightMouseDown + location:location + modifierFlags:nil + timestamp:0 + windowNumber:[[self window] windowNumber] + context:[NSGraphicsContext currentContext] + eventNumber:0 + clickCount:1 + pressure:0]; + + [self mouseEvent:fakeRightClick]; +} + +// Below is the nasty tooltip stuff -- copied from WebKit's WebHTMLView.mm +// with minor modifications for code style and commenting. +// +// The 'public' interface is -setToolTipAtMousePoint:. This differs from +// -setToolTip: in that the updated tooltip takes effect immediately, +// without the user's having to move the mouse out of and back into the view. +// +// Unfortunately, doing this requires sending fake mouseEnter/Exit events to +// the view, which in turn requires overriding some internal tracking-rect +// methods (to keep track of its owner & userdata, which need to be filled out +// in the fake events.) --snej 7/6/09 + + +/* + * Copyright (C) 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved. + * (C) 2006, 2007 Graham Dennis (graham.dennis@gmail.com) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// Any non-zero value will do, but using something recognizable might help us +// debug some day. +static const NSTrackingRectTag kTrackingRectTag = 0xBADFACE; + +// Override of a public NSView method, replacing the inherited functionality. +// See above for rationale. +- (NSTrackingRectTag)addTrackingRect:(NSRect)rect + owner:(id)owner + userData:(void *)data + assumeInside:(BOOL)assumeInside { + DCHECK(trackingRectOwner_ == nil); + trackingRectOwner_ = owner; + trackingRectUserData_ = data; + return kTrackingRectTag; +} + +// Override of (apparently) a private NSView method(!) See above for rationale. +- (NSTrackingRectTag)_addTrackingRect:(NSRect)rect + owner:(id)owner + userData:(void *)data + assumeInside:(BOOL)assumeInside + useTrackingNum:(int)tag { + DCHECK(tag == 0 || tag == kTrackingRectTag); + DCHECK(trackingRectOwner_ == nil); + trackingRectOwner_ = owner; + trackingRectUserData_ = data; + return kTrackingRectTag; +} + +// Override of (apparently) a private NSView method(!) See above for rationale. +- (void)_addTrackingRects:(NSRect *)rects + owner:(id)owner + userDataList:(void **)userDataList + assumeInsideList:(BOOL *)assumeInsideList + trackingNums:(NSTrackingRectTag *)trackingNums + count:(int)count { + DCHECK(count == 1); + DCHECK(trackingNums[0] == 0 || trackingNums[0] == kTrackingRectTag); + DCHECK(trackingRectOwner_ == nil); + trackingRectOwner_ = owner; + trackingRectUserData_ = userDataList[0]; + trackingNums[0] = kTrackingRectTag; +} + +// Override of a public NSView method, replacing the inherited functionality. +// See above for rationale. +- (void)removeTrackingRect:(NSTrackingRectTag)tag { + if (tag == 0) + return; + + if (tag == kTrackingRectTag) { + trackingRectOwner_ = nil; + return; + } + + if (tag == lastToolTipTag_) { + [super removeTrackingRect:tag]; + lastToolTipTag_ = 0; + return; + } + + // If any other tracking rect is being removed, we don't know how it was + // created and it's possible there's a leak involved (see Radar 3500217). + NOTREACHED(); +} + +// Override of (apparently) a private NSView method(!) +- (void)_removeTrackingRects:(NSTrackingRectTag *)tags count:(int)count { + for (int i = 0; i < count; ++i) { + int tag = tags[i]; + if (tag == 0) + continue; + DCHECK(tag == kTrackingRectTag); + trackingRectOwner_ = nil; + } +} + +// Sends a fake NSMouseExited event to the view for its current tracking rect. +- (void)_sendToolTipMouseExited { + // Nothing matters except window, trackingNumber, and userData. + int windowNumber = [[self window] windowNumber]; + NSEvent* fakeEvent = [NSEvent enterExitEventWithType:NSMouseExited + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:windowNumber + context:NULL + eventNumber:0 + trackingNumber:kTrackingRectTag + userData:trackingRectUserData_]; + [trackingRectOwner_ mouseExited:fakeEvent]; +} + +// Sends a fake NSMouseEntered event to the view for its current tracking rect. +- (void)_sendToolTipMouseEntered { + // Nothing matters except window, trackingNumber, and userData. + int windowNumber = [[self window] windowNumber]; + NSEvent* fakeEvent = [NSEvent enterExitEventWithType:NSMouseEntered + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:windowNumber + context:NULL + eventNumber:0 + trackingNumber:kTrackingRectTag + userData:trackingRectUserData_]; + [trackingRectOwner_ mouseEntered:fakeEvent]; +} + +// Sets the view's current tooltip, to be displayed at the current mouse +// location. (This does not make the tooltip appear -- as usual, it only +// appears after a delay.) Pass null to remove the tooltip. +- (void)setToolTipAtMousePoint:(NSString *)string { + NSString *toolTip = [string length] == 0 ? nil : string; + if ((toolTip && toolTip_ && [toolTip isEqualToString:toolTip_]) || + (!toolTip && !toolTip_)) { + return; + } + + if (toolTip_) { + [self _sendToolTipMouseExited]; + } + + toolTip_.reset([toolTip copy]); + + if (toolTip) { + // See radar 3500217 for why we remove all tooltips + // rather than just the single one we created. + [self removeAllToolTips]; + NSRect wideOpenRect = NSMakeRect(-100000, -100000, 200000, 200000); + lastToolTipTag_ = [self addToolTipRect:wideOpenRect + owner:self + userData:NULL]; + [self _sendToolTipMouseEntered]; + } +} + +// NSView calls this to get the text when displaying the tooltip. +- (NSString *)view:(NSView *)view + stringForToolTip:(NSToolTipTag)tag + point:(NSPoint)point + userData:(void *)data { + return [[toolTip_ copy] autorelease]; +} + +// Below is our NSTextInputClient implementation. +// +// When WebHTMLView receives a NSKeyDown event, WebHTMLView calls the following +// functions to process this event. +// +// [WebHTMLView keyDown] -> +// EventHandler::keyEvent() -> +// ... +// [WebEditorClient handleKeyboardEvent] -> +// [WebHTMLView _interceptEditingKeyEvent] -> +// [NSResponder interpretKeyEvents] -> +// [WebHTMLView insertText] -> +// Editor::insertText() +// +// Unfortunately, it is hard for Chromium to use this implementation because +// it causes key-typing jank. +// RenderWidgetHostViewMac is running in a browser process. On the other +// hand, Editor and EventHandler are running in a renderer process. +// So, if we used this implementation, a NSKeyDown event is dispatched to +// the following functions of Chromium. +// +// [RenderWidgetHostViewMac keyEvent] (browser) -> +// |Sync IPC (KeyDown)| (*1) -> +// EventHandler::keyEvent() (renderer) -> +// ... +// EditorClientImpl::handleKeyboardEvent() (renderer) -> +// |Sync IPC| (*2) -> +// [RenderWidgetHostViewMac _interceptEditingKeyEvent] (browser) -> +// [self interpretKeyEvents] -> +// [RenderWidgetHostViewMac insertText] (browser) -> +// |Async IPC| -> +// Editor::insertText() (renderer) +// +// (*1) we need to wait until this call finishes since WebHTMLView uses the +// result of EventHandler::keyEvent(). +// (*2) we need to wait until this call finishes since WebEditorClient uses +// the result of [WebHTMLView _interceptEditingKeyEvent]. +// +// This needs many sync IPC messages sent between a browser and a renderer for +// each key event, which would probably result in key-typing jank. +// To avoid this problem, this implementation processes key events (and input +// method events) totally in a browser process and sends asynchronous input +// events, almost same as KeyboardEvents (and TextEvents) of DOM Level 3, to a +// renderer process. +// +// [RenderWidgetHostViewMac keyEvent] (browser) -> +// |Async IPC (RawKeyDown)| -> +// [self interpretKeyEvents] -> +// [RenderWidgetHostViewMac insertText] (browser) -> +// |Async IPC (Char)| -> +// Editor::insertText() (renderer) +// +// Since this implementation doesn't have to wait any IPC calls, this doesn't +// make any key-typing jank. --hbono 7/23/09 +// +extern "C" { +extern NSString *NSTextInputReplacementRangeAttributeName; +} + +- (NSArray *)validAttributesForMarkedText { + // This code is just copied from WebKit except renaming variables. + if (!validAttributesForMarkedText_) { + validAttributesForMarkedText_.reset([[NSArray alloc] initWithObjects: + NSUnderlineStyleAttributeName, + NSUnderlineColorAttributeName, + NSMarkedClauseSegmentAttributeName, + NSTextInputReplacementRangeAttributeName, + nil]); + } + return validAttributesForMarkedText_.get(); +} + +- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint { + DCHECK([self window]); + // |thePoint| is in screen coordinates, but needs to be converted to WebKit + // coordinates (upper left origin). Scroll offsets will be taken care of in + // the renderer. + thePoint = [[self window] convertScreenToBase:thePoint]; + thePoint = [self convertPoint:thePoint fromView:nil]; + thePoint.y = NSHeight([self frame]) - thePoint.y; + + NSUInteger index = + TextInputClientMac::GetInstance()->GetCharacterIndexAtPoint( + renderWidgetHostView_->render_widget_host_, + gfx::Point(thePoint.x, thePoint.y)); + return index; +} + +- (NSRect)firstRectForCharacterRange:(NSRange)theRange + actualRange:(NSRangePointer)actualRange { + // TODO(thakis): Pipe |actualRange| through TextInputClientMac machinery. + if (actualRange) + *actualRange = theRange; + NSRect rect = TextInputClientMac::GetInstance()->GetFirstRectForRange( + renderWidgetHostView_->render_widget_host_, theRange); + + // The returned rectangle is in WebKit coordinates (upper left origin), so + // flip the coordinate system and then convert it into screen coordinates for + // return. + NSRect viewFrame = [self frame]; + rect.origin.y = NSHeight(viewFrame) - rect.origin.y; + rect.origin.y -= rect.size.height; + rect = [self convertRectToBase:rect]; + rect.origin = [[self window] convertBaseToScreen:rect.origin]; + return rect; +} + +- (NSRange)markedRange { + // An input method calls this method to check if an application really has + // a text being composed when hasMarkedText call returns true. + // Returns the range saved in the setMarkedText method so the input method + // calls the setMarkedText method and we can update the composition node + // there. (When this method returns an empty range, the input method doesn't + // call the setMarkedText method.) + return hasMarkedText_ ? markedRange_ : NSMakeRange(NSNotFound, 0); +} + +- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range + actualRange:(NSRangePointer)actualRange { + // TODO(thakis): Pipe |actualRange| through TextInputClientMac machinery. + if (actualRange) + *actualRange = range; + NSAttributedString* str = + TextInputClientMac::GetInstance()->GetAttributedSubstringFromRange( + renderWidgetHostView_->render_widget_host_, range); + return str; +} + +- (NSInteger)conversationIdentifier { + return reinterpret_cast<NSInteger>(self); +} + +// Each RenderWidgetHostViewCocoa has its own input context, but we return +// nil when the caret is in non-editable content or password box to avoid +// making input methods do their work. +- (NSTextInputContext *)inputContext { + if (focusedPluginIdentifier_ != -1) + return [[ComplexTextInputPanel sharedComplexTextInputPanel] inputContext]; + + switch(renderWidgetHostView_->text_input_type_) { + case ui::TEXT_INPUT_TYPE_NONE: + case ui::TEXT_INPUT_TYPE_PASSWORD: + return nil; + default: + return [super inputContext]; + } +} + +- (BOOL)hasMarkedText { + // An input method calls this function to figure out whether or not an + // application is really composing a text. If it is composing, it calls + // the markedRange method, and maybe calls the setMarkedText method. + // It seems an input method usually calls this function when it is about to + // cancel an ongoing composition. If an application has a non-empty marked + // range, it calls the setMarkedText method to delete the range. + return hasMarkedText_; +} + +- (void)unmarkText { + // Delete the composition node of the renderer and finish an ongoing + // composition. + // It seems an input method calls the setMarkedText method and set an empty + // text when it cancels an ongoing composition, i.e. I have never seen an + // input method calls this method. + hasMarkedText_ = NO; + markedText_.clear(); + underlines_.clear(); + + // If we are handling a key down event, then ConfirmComposition() will be + // called in keyEvent: method. + if (!handlingKeyDown_) + renderWidgetHostView_->render_widget_host_->ImeConfirmComposition(); + else + unmarkTextCalled_ = YES; +} + +- (void)setMarkedText:(id)string selectedRange:(NSRange)newSelRange + replacementRange:(NSRange)replacementRange { + // An input method updates the composition string. + // We send the given text and range to the renderer so it can update the + // composition node of WebKit. + // TODO(suzhe): It's hard for us to support replacementRange without accessing + // the full web content. + BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]]; + NSString* im_text = isAttributedString ? [string string] : string; + int length = [im_text length]; + + // |markedRange_| will get set on a callback from ImeSetComposition(). + selectedRange_ = newSelRange; + markedText_ = base::SysNSStringToUTF16(im_text); + hasMarkedText_ = (length > 0); + + underlines_.clear(); + if (isAttributedString) { + ExtractUnderlines(string, &underlines_); + } else { + // Use a thin black underline by default. + underlines_.push_back( + WebKit::WebCompositionUnderline(0, length, SK_ColorBLACK, false)); + } + + // If we are handling a key down event, then SetComposition() will be + // called in keyEvent: method. + // Input methods of Mac use setMarkedText calls with an empty text to cancel + // an ongoing composition. So, we should check whether or not the given text + // is empty to update the input method state. (Our input method backend can + // automatically cancels an ongoing composition when we send an empty text. + // So, it is OK to send an empty text to the renderer.) + if (!handlingKeyDown_) { + renderWidgetHostView_->render_widget_host_->ImeSetComposition( + markedText_, underlines_, + newSelRange.location, NSMaxRange(newSelRange)); + } +} + +- (void)doCommandBySelector:(SEL)selector { + // An input method calls this function to dispatch an editing command to be + // handled by this view. + if (selector == @selector(noop:)) + return; + + std::string command( + [RenderWidgetHostViewMacEditCommandHelper:: + CommandNameForSelector(selector) UTF8String]); + + // If this method is called when handling a key down event, then we need to + // handle the command in the key event handler. Otherwise we can just handle + // it here. + if (handlingKeyDown_) { + hasEditCommands_ = YES; + // We ignore commands that insert characters, because this was causing + // strange behavior (e.g. tab always inserted a tab rather than moving to + // the next field on the page). + if (!StartsWithASCII(command, "insert", false)) + editCommands_.push_back(EditCommand(command, "")); + } else { + RenderWidgetHost* rwh = renderWidgetHostView_->render_widget_host_; + rwh->Send(new ViewMsg_ExecuteEditCommand(rwh->routing_id(), command, "")); + } +} + +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange { + // An input method has characters to be inserted. + // Same as Linux, Mac calls this method not only: + // * when an input method finishs composing text, but also; + // * when we type an ASCII character (without using input methods). + // When we aren't using input methods, we should send the given character as + // a Char event so it is dispatched to an onkeypress() event handler of + // JavaScript. + // On the other hand, when we are using input methods, we should send the + // given characters as an input method event and prevent the characters from + // being dispatched to onkeypress() event handlers. + // Text inserting might be initiated by other source instead of keyboard + // events, such as the Characters dialog. In this case the text should be + // sent as an input method event as well. + // TODO(suzhe): It's hard for us to support replacementRange without accessing + // the full web content. NOTE: If someone adds support for this, make sure + // it works with the default range passed in by -insertText: below. + BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]]; + NSString* im_text = isAttributedString ? [string string] : string; + if (handlingKeyDown_) { + textToBeInserted_.append(base::SysNSStringToUTF16(im_text)); + } else { + renderWidgetHostView_->render_widget_host_->ImeConfirmComposition( + base::SysNSStringToUTF16(im_text)); + } + + // Inserting text will delete all marked text automatically. + hasMarkedText_ = NO; +} + +- (void)insertText:(id)string { + // This is a method on NSTextInput, not NSTextInputClient. But on 10.5, this + // gets called anyway. Forward to the right method. http://crbug.com/47890 + [self insertText:string replacementRange:NSMakeRange(0, 0)]; +} + +- (void)viewDidMoveToWindow { + if (canBeKeyView_) { + NSWindow* newWindow = [self window]; + // Pointer comparison only, since we don't know if lastWindow_ is still + // valid. + if (newWindow) { + // If we move into a new window, refresh the frame information. We + // don't need to do it if it was the same window as it used to be in, + // since that case is covered by DidBecomeSelected. We only want to + // do this for real browser views, not popups. + if (newWindow != lastWindow_) { + lastWindow_ = newWindow; + renderWidgetHostView_->WindowFrameChanged(); + } + renderWidgetHostView_->ForceTextureReload(); + } + } + + // If we switch windows (or are removed from the view hierarchy), cancel any + // open mouse-downs. + if (hasOpenMouseDown_) { + WebMouseEvent event; + event.type = WebInputEvent::MouseUp; + event.button = WebMouseEvent::ButtonLeft; + if (renderWidgetHostView_->render_widget_host_) + renderWidgetHostView_->render_widget_host_->ForwardMouseEvent(event); + + hasOpenMouseDown_ = NO; + } +} + +- (void)undo:(id)sender { + if (renderWidgetHostView_->render_widget_host_->IsRenderView()) { + static_cast<RenderViewHost*>(renderWidgetHostView_->render_widget_host_)-> + Undo(); + } +} + +- (void)redo:(id)sender { + if (renderWidgetHostView_->render_widget_host_->IsRenderView()) { + static_cast<RenderViewHost*>(renderWidgetHostView_->render_widget_host_)-> + Redo(); + } +} + +- (void)cut:(id)sender { + if (renderWidgetHostView_->render_widget_host_->IsRenderView()) { + static_cast<RenderViewHost*>(renderWidgetHostView_->render_widget_host_)-> + Cut(); + } +} + +- (void)copy:(id)sender { + if (renderWidgetHostView_->render_widget_host_->IsRenderView()) { + static_cast<RenderViewHost*>(renderWidgetHostView_->render_widget_host_)-> + Copy(); + } +} + +- (void)copyToFindPboard:(id)sender { + if (renderWidgetHostView_->render_widget_host_->IsRenderView()) { + static_cast<RenderViewHost*>(renderWidgetHostView_->render_widget_host_)-> + CopyToFindPboard(); + } +} + +- (void)paste:(id)sender { + if (renderWidgetHostView_->render_widget_host_->IsRenderView()) { + static_cast<RenderViewHost*>(renderWidgetHostView_->render_widget_host_)-> + Paste(); + } +} + +- (void)pasteAsPlainText:(id)sender { + if (renderWidgetHostView_->render_widget_host_->IsRenderView()) { + RenderWidgetHost* rwh = renderWidgetHostView_->render_widget_host_; + rwh->Send(new ViewMsg_ExecuteEditCommand( + rwh->routing_id(), "PasteAndMatchStyle", "")); + } +} + +- (void)cancelComposition { + if (!hasMarkedText_) + return; + + // Cancel the ongoing composition. [NSInputManager markedTextAbandoned:] + // doesn't call any NSTextInput functions, such as setMarkedText or + // insertText. So, we need to send an IPC message to a renderer so it can + // delete the composition node. + NSInputManager *currentInputManager = [NSInputManager currentInputManager]; + [currentInputManager markedTextAbandoned:self]; + + hasMarkedText_ = NO; + // Should not call [self unmarkText] here, because it'll send unnecessary + // cancel composition IPC message to the renderer. +} + +- (void)confirmComposition { + if (!hasMarkedText_) + return; + + if (renderWidgetHostView_->render_widget_host_) + renderWidgetHostView_->render_widget_host_->ImeConfirmComposition(); + + [self cancelComposition]; +} + +- (void)setPluginImeActive:(BOOL)active { + if (active == pluginImeActive_) + return; + + pluginImeActive_ = active; + if (!active) { + [[ComplexTextInputPanel sharedComplexTextInputPanel] cancelComposition]; + renderWidgetHostView_->PluginImeCompositionCompleted( + string16(), focusedPluginIdentifier_); + } +} + +- (void)pluginFocusChanged:(BOOL)focused forPlugin:(int)pluginId { + if (focused) + focusedPluginIdentifier_ = pluginId; + else if (focusedPluginIdentifier_ == pluginId) + focusedPluginIdentifier_ = -1; + + // Whenever plugin focus changes, plugin IME resets. + [self setPluginImeActive:NO]; +} + +- (BOOL)postProcessEventForPluginIme:(NSEvent*)event { + if (!pluginImeActive_) + return false; + + ComplexTextInputPanel* inputPanel = + [ComplexTextInputPanel sharedComplexTextInputPanel]; + NSString* composited_string = nil; + BOOL handled = [inputPanel interpretKeyEvent:event + string:&composited_string]; + if (composited_string) { + renderWidgetHostView_->PluginImeCompositionCompleted( + base::SysNSStringToUTF16(composited_string), focusedPluginIdentifier_); + pluginImeActive_ = NO; + } + return handled; +} + +- (void)checkForPluginImeCancellation { + if (pluginImeActive_ && + ![[ComplexTextInputPanel sharedComplexTextInputPanel] inComposition]) { + renderWidgetHostView_->PluginImeCompositionCompleted( + string16(), focusedPluginIdentifier_); + pluginImeActive_ = NO; + } +} + +// Overriding a NSResponder method to support application services. + +- (id)validRequestorForSendType:(NSString*)sendType + returnType:(NSString*)returnType { + id requestor = nil; + BOOL sendTypeIsString = [sendType isEqual:NSStringPboardType]; + BOOL returnTypeIsString = [returnType isEqual:NSStringPboardType]; + BOOL hasText = !renderWidgetHostView_->selected_text().empty(); + BOOL takesText = + renderWidgetHostView_->text_input_type_ != ui::TEXT_INPUT_TYPE_NONE; + + if (sendTypeIsString && hasText && !returnType) { + requestor = self; + } else if (!sendType && returnTypeIsString && takesText) { + requestor = self; + } else if (sendTypeIsString && returnTypeIsString && hasText && takesText) { + requestor = self; + } else { + requestor = [super validRequestorForSendType:sendType + returnType:returnType]; + } + return requestor; +} + +- (void)viewWillStartLiveResize { + [super viewWillStartLiveResize]; + RenderWidgetHost* widget = renderWidgetHostView_->render_widget_host_; + if (widget) + widget->Send(new ViewMsg_SetInLiveResize(widget->routing_id(), true)); +} + +- (void)viewDidEndLiveResize { + [super viewDidEndLiveResize]; + RenderWidgetHost* widget = renderWidgetHostView_->render_widget_host_; + if (widget) + widget->Send(new ViewMsg_SetInLiveResize(widget->routing_id(), false)); +} + +@end + +// +// Supporting application services +// +@implementation RenderWidgetHostViewCocoa(NSServicesRequests) + +- (BOOL)writeSelectionToPasteboard:(NSPasteboard*)pboard + types:(NSArray*)types { + const std::string& str = renderWidgetHostView_->selected_text(); + if (![types containsObject:NSStringPboardType] || str.empty()) return NO; + + scoped_nsobject<NSString> text([[NSString alloc] + initWithUTF8String:str.c_str()]); + NSArray* toDeclare = [NSArray arrayWithObject:NSStringPboardType]; + [pboard declareTypes:toDeclare owner:nil]; + return [pboard setString:text forType:NSStringPboardType]; +} + +- (BOOL)readSelectionFromPasteboard:(NSPasteboard*)pboard { + NSString *string = [pboard stringForType:NSStringPboardType]; + if (!string) return NO; + + // If the user is currently using an IME, confirm the IME input, + // and then insert the text from the service, the same as TextEdit and Safari. + [self confirmComposition]; + [self insertText:string]; + return YES; +} + +@end diff --git a/content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.h b/content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.h new file mode 100644 index 0000000..bdafbaf --- /dev/null +++ b/content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.h @@ -0,0 +1,70 @@ +// 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 CONTENT_BROWSER_RENDERER_HOST_RENDER_WIDGET_HOST_VIEW_MAC_EDITCOMMAND_HELPER_H_ +#define CONTENT_BROWSER_RENDERER_HOST_RENDER_WIDGET_HOST_VIEW_MAC_EDITCOMMAND_HELPER_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +#include "base/basictypes.h" +#include "base/hash_tables.h" +#include "base/gtest_prod_util.h" +#include "content/browser/renderer_host/render_widget_host_view_mac.h" + +// This class mimics the behavior of WebKit's WebView class in a way that makes +// sense for Chrome. +// +// WebCore has the concept of "core commands", basically named actions such as +// "Select All" and "Move Cursor Left". The commands are executed using their +// string value by WebCore. +// +// This class is responsible for 2 things: +// 1. Provide an abstraction to determine the enabled/disabled state of menu +// items that correspond to edit commands. +// 2. Hook up a bunch of objc selectors to the RenderWidgetHostViewCocoa object. +// (note that this is not a misspelling of RenderWidgetHostViewMac, it's in +// fact a distinct object) When these selectors are called, the relevant +// edit command is executed in WebCore. +class RenderWidgetHostViewMacEditCommandHelper { + FRIEND_TEST_ALL_PREFIXES(RenderWidgetHostViewMacEditCommandHelperTest, + TestAddEditingSelectorsToClass); + FRIEND_TEST_ALL_PREFIXES(RenderWidgetHostViewMacEditCommandHelperTest, + TestEditingCommandDelivery); + + public: + RenderWidgetHostViewMacEditCommandHelper(); + ~RenderWidgetHostViewMacEditCommandHelper(); + + // Adds editing selectors to the objc class using the objc runtime APIs. + // Each selector is connected to a single c method which forwards the message + // to WebCore's ExecuteEditCommand() function. + // This method is idempotent. + // The class passed in must conform to the RenderWidgetHostViewMacOwner + // protocol. + void AddEditingSelectorsToClass(Class klass); + + // Is a given menu item currently enabled? + // SEL - the objc selector currently associated with an NSMenuItem. + // owner - An object we can retrieve a RenderWidgetHostViewMac from to + // determine the command states. + bool IsMenuItemEnabled(SEL item_action, + id<RenderWidgetHostViewMacOwner> owner); + + // Converts an editing selector into a command name that can be sent to + // webkit. + static NSString* CommandNameForSelector(SEL selector); + + protected: + // Gets a list of all the selectors that AddEditingSelectorsToClass adds to + // the aforementioned class. + // returns an array of NSStrings WITHOUT the trailing ':'s. + NSArray* GetEditSelectorNames(); + + private: + base::hash_set<std::string> edit_command_set_; + DISALLOW_COPY_AND_ASSIGN(RenderWidgetHostViewMacEditCommandHelper); +}; + +#endif // CONTENT_BROWSER_RENDERER_HOST_RENDER_WIDGET_HOST_VIEW_MAC_EDITCOMMAND_HELPER_H_ diff --git a/content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.mm b/content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.mm new file mode 100644 index 0000000..4ad3b76 --- /dev/null +++ b/content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.mm @@ -0,0 +1,235 @@ +// 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. + +#import "content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.h" + +#import <objc/runtime.h> + +#include "content/browser/renderer_host/render_widget_host.h" +#import "content/browser/renderer_host/render_widget_host_view_mac.h" + +namespace { +// The names of all the objc selectors w/o ':'s added to an object by +// AddEditingSelectorsToClass(). +// +// This needs to be kept in Sync with WEB_COMMAND list in the WebKit tree at: +// WebKit/mac/WebView/WebHTMLView.mm . +const char* kEditCommands[] = { + "alignCenter", + "alignJustified", + "alignLeft", + "alignRight", + "copy", + "cut", + "delete", + "deleteBackward", + "deleteBackwardByDecomposingPreviousCharacter", + "deleteForward", + "deleteToBeginningOfLine", + "deleteToBeginningOfParagraph", + "deleteToEndOfLine", + "deleteToEndOfParagraph", + "deleteToMark", + "deleteWordBackward", + "deleteWordForward", + "ignoreSpelling", + "indent", + "insertBacktab", + "insertLineBreak", + "insertNewline", + "insertNewlineIgnoringFieldEditor", + "insertParagraphSeparator", + "insertTab", + "insertTabIgnoringFieldEditor", + "makeTextWritingDirectionLeftToRight", + "makeTextWritingDirectionNatural", + "makeTextWritingDirectionRightToLeft", + "moveBackward", + "moveBackwardAndModifySelection", + "moveDown", + "moveDownAndModifySelection", + "moveForward", + "moveForwardAndModifySelection", + "moveLeft", + "moveLeftAndModifySelection", + "moveParagraphBackwardAndModifySelection", + "moveParagraphForwardAndModifySelection", + "moveRight", + "moveRightAndModifySelection", + "moveToBeginningOfDocument", + "moveToBeginningOfDocumentAndModifySelection", + "moveToBeginningOfLine", + "moveToBeginningOfLineAndModifySelection", + "moveToBeginningOfParagraph", + "moveToBeginningOfParagraphAndModifySelection", + "moveToBeginningOfSentence", + "moveToBeginningOfSentenceAndModifySelection", + "moveToEndOfDocument", + "moveToEndOfDocumentAndModifySelection", + "moveToEndOfLine", + "moveToEndOfLineAndModifySelection", + "moveToEndOfParagraph", + "moveToEndOfParagraphAndModifySelection", + "moveToEndOfSentence", + "moveToEndOfSentenceAndModifySelection", + "moveUp", + "moveUpAndModifySelection", + "moveWordBackward", + "moveWordBackwardAndModifySelection", + "moveWordForward", + "moveWordForwardAndModifySelection", + "moveWordLeft", + "moveWordLeftAndModifySelection", + "moveWordRight", + "moveWordRightAndModifySelection", + "outdent", + "pageDown", + "pageDownAndModifySelection", + "pageUp", + "pageUpAndModifySelection", + "selectAll", + "selectLine", + "selectParagraph", + "selectSentence", + "selectToMark", + "selectWord", + "setMark", + "showGuessPanel", + "subscript", + "superscript", + "swapWithMark", + "transpose", + "underline", + "unscript", + "yank", + "yankAndSelect"}; + + +// This function is installed via the objc runtime as the implementation of all +// the various editing selectors. +// The objc runtime hookup occurs in +// RenderWidgetHostViewMacEditCommandHelper::AddEditingSelectorsToClass(). +// +// self - the object we're attached to; it must implement the +// RenderWidgetHostViewMacOwner protocol. +// _cmd - the selector that fired. +// sender - the id of the object that sent the message. +// +// The selector is translated into an edit comand and then forwarded down the +// pipeline to WebCore. +// The route the message takes is: +// RenderWidgetHostViewMac -> RenderViewHost -> +// | IPC | -> +// RenderView -> currently focused WebFrame. +// The WebFrame is in the Chrome glue layer and forwards the message to WebCore. +void EditCommandImp(id self, SEL _cmd, id sender) { + // Make sure |self| is the right type. + DCHECK([self conformsToProtocol:@protocol(RenderWidgetHostViewMacOwner)]); + + // SEL -> command name string. + NSString* command_name_ns = + RenderWidgetHostViewMacEditCommandHelper::CommandNameForSelector(_cmd); + std::string command([command_name_ns UTF8String]); + + // Forward the edit command string down the pipeline. + RenderWidgetHostViewMac* rwhv = [(id<RenderWidgetHostViewMacOwner>)self + renderWidgetHostViewMac]; + DCHECK(rwhv); + + // The second parameter is the core command value which isn't used here. + RenderWidgetHost* rwh = rwhv->GetRenderWidgetHost(); + rwh->ExecuteEditCommand(command, ""); +} + +} // namespace + +// Maps an objc-selector to a core command name. +// +// Returns the core command name (which is the selector name with the trailing +// ':' stripped in most cases). +// +// Adapted from a function by the same name in +// WebKit/mac/WebView/WebHTMLView.mm . +// Capitalized names are returned from this function, but that's simply +// matching WebHTMLView.mm. +NSString* RenderWidgetHostViewMacEditCommandHelper::CommandNameForSelector( + SEL selector) { + if (selector == @selector(insertParagraphSeparator:) || + selector == @selector(insertNewlineIgnoringFieldEditor:)) + return @"InsertNewline"; + if (selector == @selector(insertTabIgnoringFieldEditor:)) + return @"InsertTab"; + if (selector == @selector(pageDown:)) + return @"MovePageDown"; + if (selector == @selector(pageDownAndModifySelection:)) + return @"MovePageDownAndModifySelection"; + if (selector == @selector(pageUp:)) + return @"MovePageUp"; + if (selector == @selector(pageUpAndModifySelection:)) + return @"MovePageUpAndModifySelection"; + + // Remove the trailing colon. + NSString* selector_str = NSStringFromSelector(selector); + int selector_len = [selector_str length]; + return [selector_str substringToIndex:selector_len - 1]; +} + +RenderWidgetHostViewMacEditCommandHelper:: + RenderWidgetHostViewMacEditCommandHelper() { + for (size_t i = 0; i < arraysize(kEditCommands); ++i) { + edit_command_set_.insert(kEditCommands[i]); + } +} + +RenderWidgetHostViewMacEditCommandHelper:: + ~RenderWidgetHostViewMacEditCommandHelper() {} + +// Dynamically adds Selectors to the aformentioned class. +void RenderWidgetHostViewMacEditCommandHelper::AddEditingSelectorsToClass( + Class klass) { + for (size_t i = 0; i < arraysize(kEditCommands); ++i) { + // Append trailing ':' to command name to get selector name. + NSString* sel_str = [NSString stringWithFormat: @"%s:", kEditCommands[i]]; + + SEL edit_selector = NSSelectorFromString(sel_str); + // May want to use @encode() for the last parameter to this method. + // If class_addMethod fails we assume that all the editing selectors where + // added to the class. + // If a certain class already implements a method then class_addMethod + // returns NO, which we can safely ignore. + class_addMethod(klass, edit_selector, (IMP)EditCommandImp, "v@:@"); + } +} + +bool RenderWidgetHostViewMacEditCommandHelper::IsMenuItemEnabled( + SEL item_action, + id<RenderWidgetHostViewMacOwner> owner) { + const char* selector_name = sel_getName(item_action); + // TODO(jeremy): The final form of this function will check state + // associated with the Browser. + + // For now just mark all edit commands as enabled. + NSString* selector_name_ns = [NSString stringWithUTF8String:selector_name]; + + // Remove trailing ':' + size_t str_len = [selector_name_ns length]; + selector_name_ns = [selector_name_ns substringToIndex:str_len - 1]; + std::string edit_command_name([selector_name_ns UTF8String]); + + // search for presence in set and return. + bool ret = edit_command_set_.find(edit_command_name) != + edit_command_set_.end(); + return ret; +} + +NSArray* RenderWidgetHostViewMacEditCommandHelper::GetEditSelectorNames() { + size_t num_edit_commands = arraysize(kEditCommands); + NSMutableArray* ret = [NSMutableArray arrayWithCapacity:num_edit_commands]; + + for (size_t i = 0; i < num_edit_commands; ++i) { + [ret addObject:[NSString stringWithUTF8String:kEditCommands[i]]]; + } + + return ret; +} diff --git a/content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper_unittest.mm b/content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper_unittest.mm new file mode 100644 index 0000000..c81cc83 --- /dev/null +++ b/content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper_unittest.mm @@ -0,0 +1,169 @@ +// 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. + +#import "content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.h" + +#import <Cocoa/Cocoa.h> + +#include "base/message_loop.h" +#include "content/browser/renderer_host/mock_render_process_host.h" +#include "content/browser/renderer_host/render_widget_host.h" +#include "content/common/view_messages.h" +#include "content/test/test_browser_context.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +class RenderWidgetHostViewMacEditCommandHelperTest : public PlatformTest { +}; + +// Bare bones obj-c class for testing purposes. +@interface RenderWidgetHostViewMacEditCommandHelperTestClass : NSObject +@end + +@implementation RenderWidgetHostViewMacEditCommandHelperTestClass +@end + +// Class that owns a RenderWidgetHostViewMac. +@interface RenderWidgetHostViewMacOwner : + NSObject<RenderWidgetHostViewMacOwner> { + RenderWidgetHostViewMac* rwhvm_; +} + +- (id)initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)rwhvm; +@end + +@implementation RenderWidgetHostViewMacOwner + +- (id)initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)rwhvm { + if ((self = [super init])) { + rwhvm_ = rwhvm; + } + return self; +} + +- (RenderWidgetHostViewMac*)renderWidgetHostViewMac { + return rwhvm_; +} + +@end + + +namespace { + // Returns true if all the edit command names in the array are present + // in test_obj. + // edit_commands is a list of NSStrings, selector names are formed by + // appending a trailing ':' to the string. + bool CheckObjectRespondsToEditCommands(NSArray* edit_commands, + id test_obj) { + for (NSString* edit_command_name in edit_commands) { + NSString* sel_str = [edit_command_name stringByAppendingString:@":"]; + if (![test_obj respondsToSelector:NSSelectorFromString(sel_str)]) { + return false; + } + } + + return true; + } +} // namespace + +// Create a RenderWidget for which we can filter messages. +class RenderWidgetHostEditCommandCounter : public RenderWidgetHost { + public: + RenderWidgetHostEditCommandCounter(RenderProcessHost* process, + int routing_id) + : RenderWidgetHost(process, routing_id), + edit_command_message_count_(0) { + } + + virtual bool Send(IPC::Message* message) { + if (message->type() == ViewMsg_ExecuteEditCommand::ID) + edit_command_message_count_++; + return RenderWidgetHost::Send(message); + } + + unsigned int edit_command_message_count_; +}; + + +// Tests that editing commands make it through the pipeline all the way to +// RenderWidgetHost. +// Disabled, http://crbug.com/93286. +TEST_F(RenderWidgetHostViewMacEditCommandHelperTest, + DISABLED_TestEditingCommandDelivery) { + RenderWidgetHostViewMacEditCommandHelper helper; + NSArray* edit_command_strings = helper.GetEditSelectorNames(); + + // Set up a mock render widget and set expectations. + MessageLoopForUI message_loop; + TestBrowserContext browser_context; + MockRenderProcessHost mock_process(&browser_context); + RenderWidgetHostEditCommandCounter render_widget(&mock_process, 0); + + // RenderWidgetHostViewMac self destructs (RenderWidgetHostViewMacCocoa + // takes ownership) so no need to delete it ourselves. + RenderWidgetHostViewMac* rwhvm = new RenderWidgetHostViewMac(&render_widget); + + RenderWidgetHostViewMacOwner* rwhwvm_owner = + [[[RenderWidgetHostViewMacOwner alloc] + initWithRenderWidgetHostViewMac:rwhvm] autorelease]; + + helper.AddEditingSelectorsToClass([rwhwvm_owner class]); + + for (NSString* edit_command_name in edit_command_strings) { + NSString* sel_str = [edit_command_name stringByAppendingString:@":"]; + [rwhwvm_owner performSelector:NSSelectorFromString(sel_str) withObject:nil]; + } + + size_t num_edit_commands = [edit_command_strings count]; + EXPECT_EQ(render_widget.edit_command_message_count_, num_edit_commands); +} + +// Test RenderWidgetHostViewMacEditCommandHelper::AddEditingSelectorsToClass +TEST_F(RenderWidgetHostViewMacEditCommandHelperTest, + TestAddEditingSelectorsToClass) { + RenderWidgetHostViewMacEditCommandHelper helper; + NSArray* edit_command_strings = helper.GetEditSelectorNames(); + ASSERT_GT([edit_command_strings count], 0U); + + // Create a class instance and add methods to the class. + RenderWidgetHostViewMacEditCommandHelperTestClass* test_obj = + [[[RenderWidgetHostViewMacEditCommandHelperTestClass alloc] init] + autorelease]; + + // Check that edit commands aren't already attached to the object. + ASSERT_FALSE(CheckObjectRespondsToEditCommands(edit_command_strings, + test_obj)); + + helper.AddEditingSelectorsToClass([test_obj class]); + + // Check that all edit commands where added. + ASSERT_TRUE(CheckObjectRespondsToEditCommands(edit_command_strings, + test_obj)); + + // AddEditingSelectorsToClass() should be idempotent. + helper.AddEditingSelectorsToClass([test_obj class]); + + // Check that all edit commands are still there. + ASSERT_TRUE(CheckObjectRespondsToEditCommands(edit_command_strings, + test_obj)); +} + +// Test RenderWidgetHostViewMacEditCommandHelper::IsMenuItemEnabled. +TEST_F(RenderWidgetHostViewMacEditCommandHelperTest, TestMenuItemEnabling) { + RenderWidgetHostViewMacEditCommandHelper helper; + RenderWidgetHostViewMacOwner* rwhvm_owner = + [[[RenderWidgetHostViewMacOwner alloc] init] autorelease]; + + // The select all menu should always be enabled. + SEL select_all = NSSelectorFromString(@"selectAll:"); + ASSERT_TRUE(helper.IsMenuItemEnabled(select_all, rwhvm_owner)); + + // Random selectors should be enabled by the function. + SEL garbage_selector = NSSelectorFromString(@"randomGarbageSelector:"); + ASSERT_FALSE(helper.IsMenuItemEnabled(garbage_selector, rwhvm_owner)); + + // TODO(jeremy): Currently IsMenuItemEnabled just returns true for all edit + // selectors. Once we go past that we should do more extensive testing here. +} diff --git a/content/browser/renderer_host/render_widget_host_view_mac_unittest.mm b/content/browser/renderer_host/render_widget_host_view_mac_unittest.mm new file mode 100644 index 0000000..43e5300 --- /dev/null +++ b/content/browser/renderer_host/render_widget_host_view_mac_unittest.mm @@ -0,0 +1,194 @@ +// 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 "content/browser/renderer_host/render_widget_host_view_mac.h" + +#include "base/mac/scoped_nsautorelease_pool.h" +#include "content/browser/browser_thread.h" +#include "content/browser/renderer_host/test_render_view_host.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/test/cocoa_test_event_utils.h" +#import "ui/base/test/ui_cocoa_test_helper.h" +#include "webkit/plugins/npapi/webplugin.h" + +class RenderWidgetHostViewMacTest : public RenderViewHostTestHarness { + public: + RenderWidgetHostViewMacTest() : old_rwhv_(NULL), rwhv_mac_(NULL) {} + + virtual void SetUp() { + RenderViewHostTestHarness::SetUp(); + + // TestRenderViewHost's destruction assumes that its view is a + // TestRenderWidgetHostView, so store its view and reset it back to the + // stored view in |TearDown()|. + old_rwhv_ = rvh()->view(); + + // Owned by its |native_view()|, i.e. |rwhv_cocoa_|. + rwhv_mac_ = new RenderWidgetHostViewMac(rvh()); + rwhv_cocoa_.reset([rwhv_mac_->native_view() retain]); + } + virtual void TearDown() { + // See comment in SetUp(). + rvh()->SetView(old_rwhv_); + + // Make sure the rwhv_mac_ is gone once the superclass's |TearDown()| runs. + rwhv_cocoa_.reset(); + pool_.Recycle(); + MessageLoop::current()->RunAllPending(); + pool_.Recycle(); + + RenderViewHostTestHarness::TearDown(); + } + protected: + // Adds an accelerated plugin view to |rwhv_cocoa_|. Returns a handle to the + // newly-added view. Callers must ensure that a UI thread is present and + // running before calling this function. + gfx::PluginWindowHandle AddAcceleratedPluginView(int w, int h) { + // Create an accelerated view the size of the rhwvmac. + [rwhv_cocoa_.get() setFrame:NSMakeRect(0, 0, w, h)]; + gfx::PluginWindowHandle accelerated_handle = + rwhv_mac_->AllocateFakePluginWindowHandle(/*opaque=*/false, + /*root=*/false); + rwhv_mac_->AcceleratedSurfaceSetIOSurface(accelerated_handle, w, h, 0); + + // The accelerated view isn't shown until it has a valid rect and has been + // painted to. + rwhv_mac_->AcceleratedSurfaceBuffersSwapped(accelerated_handle, + 0, 0, 0, 0, 0); + webkit::npapi::WebPluginGeometry geom; + gfx::Rect rect(0, 0, w, h); + geom.window = accelerated_handle; + geom.window_rect = rect; + geom.clip_rect = rect; + geom.visible = true; + geom.rects_valid = true; + rwhv_mac_->MovePluginWindows( + std::vector<webkit::npapi::WebPluginGeometry>(1, geom)); + + return accelerated_handle; + } + private: + // This class isn't derived from PlatformTest. + base::mac::ScopedNSAutoreleasePool pool_; + + RenderWidgetHostView* old_rwhv_; + + protected: + RenderWidgetHostViewMac* rwhv_mac_; + scoped_nsobject<RenderWidgetHostViewCocoa> rwhv_cocoa_; + + private: + DISALLOW_COPY_AND_ASSIGN(RenderWidgetHostViewMacTest); +}; + +TEST_F(RenderWidgetHostViewMacTest, Basic) { +} + +// Regression test for http://crbug.com/60318 +TEST_F(RenderWidgetHostViewMacTest, FocusAcceleratedView) { + // The accelerated view methods want to be called on the UI thread. + scoped_ptr<BrowserThread> ui_thread_( + new BrowserThread(BrowserThread::UI, MessageLoop::current())); + + int w = 400, h = 300; + gfx::PluginWindowHandle accelerated_handle = AddAcceleratedPluginView(w, h); + EXPECT_FALSE([rwhv_cocoa_.get() isHidden]); + NSView* accelerated_view = static_cast<NSView*>( + rwhv_mac_->ViewForPluginWindowHandle(accelerated_handle)); + EXPECT_FALSE([accelerated_view isHidden]); + + // Take away first responder from the rwhvmac, then simulate the effect of a + // click on the accelerated view. The rwhvmac should be first responder + // again. + scoped_nsobject<NSWindow> window([[CocoaTestHelperWindow alloc] init]); + scoped_nsobject<NSView> other_view( + [[NSTextField alloc] initWithFrame:NSMakeRect(0, h, w, 40)]); + [[window contentView] addSubview:rwhv_cocoa_.get()]; + [[window contentView] addSubview:other_view.get()]; + + EXPECT_TRUE([rwhv_cocoa_.get() acceptsFirstResponder]); + [window makeFirstResponder:rwhv_cocoa_.get()]; + EXPECT_EQ(rwhv_cocoa_.get(), [window firstResponder]); + EXPECT_FALSE([accelerated_view acceptsFirstResponder]); + + EXPECT_TRUE([other_view acceptsFirstResponder]); + [window makeFirstResponder:other_view]; + EXPECT_NE(rwhv_cocoa_.get(), [window firstResponder]); + + EXPECT_TRUE([accelerated_view acceptsFirstResponder]); + [window makeFirstResponder:accelerated_view]; + EXPECT_EQ(rwhv_cocoa_.get(), [window firstResponder]); + + // Clean up. + rwhv_mac_->DestroyFakePluginWindowHandle(accelerated_handle); +} + +TEST_F(RenderWidgetHostViewMacTest, AcceptsFirstResponder) { + // The RWHVCocoa should normally accept first responder status. + EXPECT_TRUE([rwhv_cocoa_.get() acceptsFirstResponder]); + + // Unless we tell it not to. + rwhv_mac_->SetTakesFocusOnlyOnMouseDown(true); + EXPECT_FALSE([rwhv_cocoa_.get() acceptsFirstResponder]); + + // But we can set things back to the way they were originally. + rwhv_mac_->SetTakesFocusOnlyOnMouseDown(false); + EXPECT_TRUE([rwhv_cocoa_.get() acceptsFirstResponder]); +} + +TEST_F(RenderWidgetHostViewMacTest, TakesFocusOnMouseDown) { + scoped_nsobject<CocoaTestHelperWindow> + window([[CocoaTestHelperWindow alloc] init]); + [[window contentView] addSubview:rwhv_cocoa_.get()]; + + // Even if the RWHVCocoa disallows first responder, clicking on it gives it + // focus. + [window setPretendIsKeyWindow:YES]; + [window makeFirstResponder:nil]; + ASSERT_NE(rwhv_cocoa_.get(), [window firstResponder]); + + rwhv_mac_->SetTakesFocusOnlyOnMouseDown(true); + EXPECT_FALSE([rwhv_cocoa_.get() acceptsFirstResponder]); + + std::pair<NSEvent*, NSEvent*> clicks = + cocoa_test_event_utils::MouseClickInView(rwhv_cocoa_.get(), 1); + [rwhv_cocoa_.get() mouseDown:clicks.first]; + EXPECT_EQ(rwhv_cocoa_.get(), [window firstResponder]); +} + +// Regression test for http://crbug.com/64256 +TEST_F(RenderWidgetHostViewMacTest, TakesFocusOnMouseDownWithAcceleratedView) { + // The accelerated view methods want to be called on the UI thread. + scoped_ptr<BrowserThread> ui_thread_( + new BrowserThread(BrowserThread::UI, MessageLoop::current())); + + int w = 400, h = 300; + gfx::PluginWindowHandle accelerated_handle = AddAcceleratedPluginView(w, h); + EXPECT_FALSE([rwhv_cocoa_.get() isHidden]); + NSView* accelerated_view = static_cast<NSView*>( + rwhv_mac_->ViewForPluginWindowHandle(accelerated_handle)); + EXPECT_FALSE([accelerated_view isHidden]); + + // Add the RWHVCocoa to the window and remove first responder status. + scoped_nsobject<CocoaTestHelperWindow> + window([[CocoaTestHelperWindow alloc] init]); + [[window contentView] addSubview:rwhv_cocoa_.get()]; + [window setPretendIsKeyWindow:YES]; + [window makeFirstResponder:nil]; + EXPECT_NE(rwhv_cocoa_.get(), [window firstResponder]); + + // Tell the RWHVMac to not accept first responder status. The accelerated + // view should also stop accepting first responder. + rwhv_mac_->SetTakesFocusOnlyOnMouseDown(true); + EXPECT_FALSE([accelerated_view acceptsFirstResponder]); + + // A click on the accelerated view should focus the RWHVCocoa. + std::pair<NSEvent*, NSEvent*> clicks = + cocoa_test_event_utils::MouseClickInView(accelerated_view, 1); + [rwhv_cocoa_.get() mouseDown:clicks.first]; + EXPECT_EQ(rwhv_cocoa_.get(), [window firstResponder]); + + // Clean up. + rwhv_mac_->DestroyFakePluginWindowHandle(accelerated_handle); +} |