blob: 962a617dacde0a45670035bccbffa3d85502911c [file] [log] [blame]
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/renderer_host/render_widget_host_view_mac.h"
#include <QuartzCore/QuartzCore.h>
#include <cmath>
#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"
#include "chrome/browser/browser_trial.h"
#include "chrome/browser/mac/closure_blocks_leopard_compat.h"
#import "chrome/browser/renderer_host/accelerated_plugin_view_mac.h"
#import "chrome/browser/renderer_host/text_input_client_mac.h"
#include "chrome/browser/spellchecker/spellchecker_platform_engine.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#import "chrome/browser/ui/cocoa/history_overlay_controller.h"
#import "chrome/browser/ui/cocoa/rwhvm_editcommand_helper.h"
#import "chrome/browser/ui/cocoa/view_id_util.h"
#include "chrome/common/render_messages.h"
#include "chrome/common/spellcheck_messages.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/plugin_process_host.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"
#include "content/browser/renderer_host/render_view_host_observer.h"
#include "content/browser/renderer_host/render_widget_host.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
enum {
NSEventPhaseNone = 0, // event not associated with a phase.
NSEventPhaseBegan = 0x1 << 0,
NSEventPhaseStationary = 0x1 << 1,
NSEventPhaseChanged = 0x1 << 2,
NSEventPhaseEnded = 0x1 << 3,
NSEventPhaseCancelled = 0x1 << 4,
};
typedef NSUInteger NSEventPhase;
enum {
NSEventSwipeTrackingLockDirection = 0x1 << 0,
NSEventSwipeTrackingClampGestureAmount = 0x1 << 1
};
typedef NSUInteger NSEventSwipeTrackingOptions;
@interface NSEvent (LionAPI)
+ (BOOL)isSwipeTrackingFromScrollEventsEnabled;
+ (id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask
handler:(NSEvent* (^)(NSEvent*))block;
+ (void)removeMonitor:(id)eventMonitor;
- (NSEventPhase)phase;
- (CGFloat)scrollingDeltaX;
- (CGFloat)scrollingDeltaY;
- (void)trackSwipeEventWithOptions:(NSEventSwipeTrackingOptions)options
dampenAmountThresholdMin:(CGFloat)minDampenThreshold
max:(CGFloat)maxDampenThreshold
usingHandler:(void (^)(CGFloat gestureAmount,
NSEventPhase phase,
BOOL isComplete,
BOOL *stop))trackingHandler;
@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)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;
}
// Filters the message sent to RenderViewHost to know if spellchecking is
// enabled or not for the currently focused element.
class SpellCheckRenderViewObserver : public RenderViewHostObserver {
public:
SpellCheckRenderViewObserver(RenderViewHost* host,
RenderWidgetHostViewMac* view)
: RenderViewHostObserver(host),
view_(view) {
}
private:
// RenderViewHostObserver implementation.
virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(SpellCheckRenderViewObserver, message)
IPC_MESSAGE_HANDLER(SpellCheckHostMsg_ToggleSpellCheck,
OnToggleSpellCheck)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
void OnToggleSpellCheck(bool enabled, bool checked) {
view_->ToggleSpellCheck(enabled, checked);
}
RenderWidgetHostViewMac* view_;
};
} // 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),
spellcheck_enabled_(false),
spellcheck_checked_(false),
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);
if (render_widget_host_->IsRenderView()) {
new SpellCheckRenderViewObserver(
static_cast<RenderViewHost*>(widget), this);
}
}
RenderWidgetHostViewMac::~RenderWidgetHostViewMac() {
}
///////////////////////////////////////////////////////////////////////////////
// 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 std::wstring& 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.
std::wstring display_text = tooltip_text_;
if (tooltip_text_.length() > kMaxTooltipLength)
display_text = tooltip_text_.substr(0, kMaxTooltipLength);
NSString* tooltip_nsstring = base::SysWideToNSString(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::ToggleSpellCheck(bool enabled, bool checked) {
spellcheck_enabled_ = enabled;
spellcheck_checked_ = checked;
}
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_ setGotUnhandledWheelEvent:YES];
}
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_ setPinnedLeft:is_pinned_to_left];
[cocoa_view_ setPinnedRight:is_pinned_to_right];
}
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_;
@synthesize gotUnhandledWheelEvent = gotUnhandledWheelEvent_;
@synthesize isPinnedLeft = isPinnedLeft_;
@synthesize isPinnedRight = isPinnedRight_;
@synthesize hasHorizontalScrollbar = hasHorizontalScrollbar_;
- (id)initWithRenderWidgetHostViewMac:(RenderWidgetHostViewMac*)r {
self = [super initWithFrame:NSZeroRect];
if (self) {
editCommand_helper_.reset(new RWHVMEditCommandHelper);
editCommand_helper_->AddEditingSelectorsToClass([self class]);
renderWidgetHostView_.reset(r);
canBeKeyView_ = YES;
focusedPluginIdentifier_ = -1;
}
return self;
}
- (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 ([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 {
[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];
}
// Checks if |theEvent| should trigger history swiping, and if so, does
// history swiping. Returns YES if the event was consumed or NO if it should
// be passed on to the renderer.
- (BOOL)maybeHandleHistorySwiping:(NSEvent*)theEvent {
BOOL canUseLionApis = [theEvent respondsToSelector:@selector(phase)];
if (!canUseLionApis)
return NO;
// Scroll events always go to the web first, and can only trigger history
// swiping if they come back unhandled.
if ([theEvent phase] == NSEventPhaseBegan) {
totalScrollDelta_ = NSZeroSize;
gotUnhandledWheelEvent_ = false;
}
if (gotUnhandledWheelEvent_ &&
[NSEvent isSwipeTrackingFromScrollEventsEnabled] &&
[theEvent phase] == NSEventPhaseChanged) {
totalScrollDelta_.width += [theEvent scrollingDeltaX];
totalScrollDelta_.height += [theEvent scrollingDeltaY];
bool isHorizontalGesture =
std::abs(totalScrollDelta_.width) > std::abs(totalScrollDelta_.height);
bool isRightScroll = [theEvent scrollingDeltaX] < 0;
bool goForward = isRightScroll;
bool canGoBack = false, canGoForward = false;
if (Browser* browser = BrowserList::GetLastActive()) {
canGoBack = browser->CanGoBack();
canGoForward = browser->CanGoForward();
}
// If "forward" is inactive and the user rubber-bands to the right,
// "isPinnedLeft" will be false. When the user then rubber-bands to the
// left in the same gesture, that should trigger history immediately if
// there's no scrollbar, hence the check for hasHorizontalScrollbar_.
bool shouldGoBack = isPinnedLeft_ || !hasHorizontalScrollbar_;
bool shouldGoForward = isPinnedRight_ || !hasHorizontalScrollbar_;
if (isHorizontalGesture &&
// For normal pages, canGoBack/canGoForward are checked in the renderer
// (ChromeClientImpl::shouldRubberBand()), when it decides if it should
// rubberband or send back an event unhandled. The check here is
// required for pages with an onmousewheel handler that doesn't call
// preventDefault().
((shouldGoBack && canGoBack && !isRightScroll) ||
(shouldGoForward && canGoForward && isRightScroll))) {
// Released by the tracking handler once the gesture is complete.
HistoryOverlayController* historyOverlay =
[[HistoryOverlayController alloc]
initForMode:goForward ? kHistoryOverlayModeForward :
kHistoryOverlayModeBack];
// The way this api works: gestureAmount is between -1 and 1 (float). If
// the user does the gesture for more than about 25% (i.e. < -0.25 or >
// 0.25) and then lets go, it is accepted, we get a NSEventPhaseEnded,
// and after that the block is called with amounts animating towards 1
// (or -1, depending on the direction). If the user lets go below that
// threshold, we get NSEventPhaseCancelled, and the amount animates
// toward 0. When gestureAmount has reaches its final value, i.e. the
// track animation is done, the handler is called with |isComplete| set
// to |YES|.
[theEvent trackSwipeEventWithOptions:0
dampenAmountThresholdMin:-1
max:1
usingHandler:^(CGFloat gestureAmount,
NSEventPhase phase,
BOOL isComplete,
BOOL *stop) {
if (phase == NSEventPhaseBegan) {
[historyOverlay showPanelForWindow:[self window]];
return;
}
// |gestureAmount| obeys -[NSEvent isDirectionInvertedFromDevice]
// automatically.
Browser* browser = BrowserList::GetLastActive();
if (phase == NSEventPhaseEnded && browser) {
if (goForward)
browser->GoForward(CURRENT_TAB);
else
browser->GoBack(CURRENT_TAB);
}
[historyOverlay setProgress:gestureAmount];
if (isComplete) {
[historyOverlay dismiss];
[historyOverlay release];
}
}];
return YES;
}
}
return NO;
}
- (void)scrollWheel:(NSEvent*)theEvent {
[self cancelChildPopups];
if ([self maybeHandleHistorySwiping:theEvent])
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 {
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:) ||
action == @selector(checkSpelling:)) {
return renderWidgetHostView_->render_widget_host_->IsRenderView();
}
if (action == @selector(toggleContinuousSpellChecking:)) {
if ([(id)item respondsToSelector:@selector(setState:)]) {
NSCellStateValue checked_state =
renderWidgetHostView_->spellcheck_checked_ ?
NSOnState : NSOffState;
[(id)item setState:checked_state];
}
return renderWidgetHostView_->spellcheck_enabled_;
}
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];
}
// Spellchecking methods
// The next three methods are implemented here since this class is the first
// responder for anything in the browser.
// This message is sent whenever the user specifies that a word should be
// changed from the spellChecker.
- (void)changeSpelling:(id)sender {
// Grab the currently selected word from the spell panel, as this is the word
// that we want to replace the selected word in the text with.
NSString* newWord = [[sender selectedCell] stringValue];
if (newWord != nil) {
RenderWidgetHostViewMac* thisHostView = [self renderWidgetHostViewMac];
thisHostView->GetRenderWidgetHost()->Replace(
base::SysNSStringToUTF16(newWord));
}
}
// This message is sent by NSSpellChecker whenever the next word should be
// advanced to, either after a correction or clicking the "Find Next" button.
// This isn't documented anywhere useful, like in NSSpellProtocol.h with the
// other spelling panel methods. This is probably because Apple assumes that the
// the spelling panel will be used with an NSText, which will automatically
// catch this and advance to the next word for you. Thanks Apple.
// This is also called from the Edit -> Spelling -> Check Spelling menu item.
- (void)checkSpelling:(id)sender {
RenderWidgetHostViewMac* thisHostView = [self renderWidgetHostViewMac];
RenderWidgetHost* widget = thisHostView->GetRenderWidgetHost();
widget->Send(new SpellCheckMsg_AdvanceToNextMisspelling(
widget->routing_id()));
}
// This message is sent by the spelling panel whenever a word is ignored.
- (void)ignoreSpelling:(id)sender {
// Ideally, we would ask the current RenderView for its tag, but that would
// mean making a blocking IPC call from the browser. Instead,
// SpellCheckerPlatform::CheckSpelling remembers the last tag and
// SpellCheckerPlatform::IgnoreWord assumes that is the correct tag.
NSString* wordToIgnore = [sender stringValue];
if (wordToIgnore != nil)
SpellCheckerPlatform::IgnoreWord(base::SysNSStringToUTF16(wordToIgnore));
}
- (void)showGuessPanel:(id)sender {
RenderWidgetHostViewMac* thisHostView = [self renderWidgetHostViewMac];
RenderWidgetHost* widget = thisHostView->GetRenderWidgetHost();
widget->Send(new SpellCheckMsg_ToggleSpellPanel(
widget->routing_id(), SpellCheckerPlatform::SpellingPanelVisible()));
}
- (void)toggleContinuousSpellChecking:(id)sender {
RenderWidgetHost* widget = renderWidgetHostView_->render_widget_host_;
widget->Send(new SpellCheckMsg_ToggleSpellCheck(widget->routing_id()));
}
// END Spellchecking methods
// 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(
[RWHVMEditCommandHelper::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;
}
}
- (ViewID)viewID {
return VIEW_ID_TAB_CONTAINER_FOCUS_VIEW;
}
// 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