chrome_render_widget_host_view_mac_delegate.mm revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "chrome/browser/renderer_host/chrome_render_widget_host_view_mac_delegate.h" 6 7#include <cmath> 8 9#include "base/prefs/pref_service.h" 10#include "base/strings/sys_string_conversions.h" 11#include "chrome/browser/devtools/devtools_window.h" 12#include "chrome/browser/profiles/profile.h" 13#include "chrome/browser/spellchecker/spellcheck_platform_mac.h" 14#include "chrome/browser/ui/browser.h" 15#include "chrome/browser/ui/browser_commands.h" 16#include "chrome/browser/ui/browser_finder.h" 17#import "chrome/browser/ui/cocoa/browser_window_controller.h" 18#import "chrome/browser/ui/cocoa/history_overlay_controller.h" 19#import "chrome/browser/ui/cocoa/tab_contents/overlayable_contents_controller.h" 20#import "chrome/browser/ui/cocoa/view_id_util.h" 21#include "chrome/common/pref_names.h" 22#include "chrome/common/spellcheck_messages.h" 23#include "chrome/common/url_constants.h" 24#include "content/public/browser/render_process_host.h" 25#include "content/public/browser/render_view_host.h" 26#include "content/public/browser/render_view_host_observer.h" 27#include "content/public/browser/render_widget_host.h" 28#include "content/public/browser/render_widget_host_view.h" 29#include "content/public/browser/web_contents.h" 30 31using content::RenderViewHost; 32 33// Declare things that are part of the 10.7 SDK. 34#if !defined(MAC_OS_X_VERSION_10_7) || \ 35 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7 36enum { 37 NSEventPhaseNone = 0, // event not associated with a phase. 38 NSEventPhaseBegan = 0x1 << 0, 39 NSEventPhaseStationary = 0x1 << 1, 40 NSEventPhaseChanged = 0x1 << 2, 41 NSEventPhaseEnded = 0x1 << 3, 42 NSEventPhaseCancelled = 0x1 << 4, 43}; 44typedef NSUInteger NSEventPhase; 45 46enum { 47 NSEventSwipeTrackingLockDirection = 0x1 << 0, 48 NSEventSwipeTrackingClampGestureAmount = 0x1 << 1 49}; 50typedef NSUInteger NSEventSwipeTrackingOptions; 51 52@interface NSEvent (LionAPI) 53+ (BOOL)isSwipeTrackingFromScrollEventsEnabled; 54 55- (NSEventPhase)phase; 56- (CGFloat)scrollingDeltaX; 57- (CGFloat)scrollingDeltaY; 58- (void)trackSwipeEventWithOptions:(NSEventSwipeTrackingOptions)options 59 dampenAmountThresholdMin:(CGFloat)minDampenThreshold 60 max:(CGFloat)maxDampenThreshold 61 usingHandler:(void (^)(CGFloat gestureAmount, 62 NSEventPhase phase, 63 BOOL isComplete, 64 BOOL *stop))trackingHandler; 65@end 66#endif // 10.7 67 68@interface ChromeRenderWidgetHostViewMacDelegate () 69- (BOOL)maybeHandleHistorySwiping:(NSEvent*)theEvent; 70- (void)spellCheckEnabled:(BOOL)enabled checked:(BOOL)checked; 71@end 72 73namespace ChromeRenderWidgetHostViewMacDelegateInternal { 74 75// Filters the message sent to RenderViewHost to know if spellchecking is 76// enabled or not for the currently focused element. 77class SpellCheckRenderViewObserver : public content::RenderViewHostObserver { 78 public: 79 SpellCheckRenderViewObserver( 80 RenderViewHost* host, 81 ChromeRenderWidgetHostViewMacDelegate* view_delegate) 82 : content::RenderViewHostObserver(host), 83 view_delegate_(view_delegate) { 84 } 85 86 virtual ~SpellCheckRenderViewObserver() { 87 } 88 89 private: 90 // content::RenderViewHostObserver implementation. 91 virtual void RenderViewHostDestroyed(RenderViewHost* rvh) OVERRIDE { 92 // The parent implementation destroys the observer, scoping the lifetime of 93 // the observer to the RenderViewHost. Since this class is acting as a 94 // bridge to the view for the delegate below, and is owned by that delegate, 95 // undo the scoping by not calling through to the parent implementation. 96 } 97 98 virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE { 99 bool handled = true; 100 IPC_BEGIN_MESSAGE_MAP(SpellCheckRenderViewObserver, message) 101 IPC_MESSAGE_HANDLER(SpellCheckHostMsg_ToggleSpellCheck, 102 OnToggleSpellCheck) 103 IPC_MESSAGE_UNHANDLED(handled = false) 104 IPC_END_MESSAGE_MAP() 105 return handled; 106 } 107 108 void OnToggleSpellCheck(bool enabled, bool checked) { 109 [view_delegate_ spellCheckEnabled:enabled checked:checked]; 110 } 111 112 ChromeRenderWidgetHostViewMacDelegate* view_delegate_; 113}; 114 115} // namespace ChromeRenderWidgetHostViewMacDelegateInternal 116 117@implementation ChromeRenderWidgetHostViewMacDelegate 118 119- (id)initWithRenderWidgetHost:(content::RenderWidgetHost*)renderWidgetHost { 120 self = [super init]; 121 if (self) { 122 renderWidgetHost_ = renderWidgetHost; 123 NSView* nativeView = renderWidgetHost_->GetView()->GetNativeView(); 124 view_id_util::SetID(nativeView, VIEW_ID_TAB_CONTAINER); 125 126 if (renderWidgetHost_->IsRenderView()) { 127 spellingObserver_.reset( 128 new ChromeRenderWidgetHostViewMacDelegateInternal:: 129 SpellCheckRenderViewObserver( 130 RenderViewHost::From(renderWidgetHost_), self)); 131 } 132 } 133 return self; 134} 135 136- (void)viewGone:(NSView*)view { 137 view_id_util::UnsetID(view); 138 [self autorelease]; 139} 140 141- (BOOL)handleEvent:(NSEvent*)event { 142 if ([event type] == NSScrollWheel) 143 return [self maybeHandleHistorySwiping:event]; 144 145 return NO; 146} 147 148- (void)gotUnhandledWheelEvent { 149 gotUnhandledWheelEvent_ = YES; 150} 151 152- (void)scrollOffsetPinnedToLeft:(BOOL)left toRight:(BOOL)right { 153 isPinnedLeft_ = left; 154 isPinnedRight_ = right; 155} 156 157- (void)setHasHorizontalScrollbar:(BOOL)hasHorizontalScrollbar { 158 hasHorizontalScrollbar_ = hasHorizontalScrollbar; 159} 160 161// Checks if |theEvent| should trigger history swiping, and if so, does 162// history swiping. Returns YES if the event was consumed or NO if it should 163// be passed on to the renderer. 164- (BOOL)maybeHandleHistorySwiping:(NSEvent*)theEvent { 165 BOOL canUseLionApis = [theEvent respondsToSelector:@selector(phase)]; 166 if (!canUseLionApis) 167 return NO; 168 169 // Scroll events always go to the web first, and can only trigger history 170 // swiping if they come back unhandled. 171 if ([theEvent phase] == NSEventPhaseBegan) { 172 totalScrollDelta_ = NSZeroSize; 173 gotUnhandledWheelEvent_ = NO; 174 } 175 176 if (!renderWidgetHost_ || !renderWidgetHost_->IsRenderView()) 177 return NO; 178 if (DevToolsWindow::IsDevToolsWindow( 179 RenderViewHost::From(renderWidgetHost_))) { 180 return NO; 181 } 182 183 if (gotUnhandledWheelEvent_ && 184 [NSEvent isSwipeTrackingFromScrollEventsEnabled] && 185 [theEvent phase] == NSEventPhaseChanged) { 186 Browser* browser = chrome::FindBrowserWithWindow([theEvent window]); 187 totalScrollDelta_.width += [theEvent scrollingDeltaX]; 188 totalScrollDelta_.height += [theEvent scrollingDeltaY]; 189 190 bool isHorizontalGesture = 191 std::abs(totalScrollDelta_.width) > std::abs(totalScrollDelta_.height); 192 193 bool isRightScroll = [theEvent scrollingDeltaX] < 0; 194 bool goForward = isRightScroll; 195 bool canGoBack = false, canGoForward = false; 196 if (browser) { 197 canGoBack = chrome::CanGoBack(browser); 198 canGoForward = chrome::CanGoForward(browser); 199 } 200 201 // If "forward" is inactive and the user rubber-bands to the right, 202 // "isPinnedLeft" will be false. When the user then rubber-bands to the 203 // left in the same gesture, that should trigger history immediately if 204 // there's no scrollbar, hence the check for hasHorizontalScrollbar_. 205 bool shouldGoBack = isPinnedLeft_ || !hasHorizontalScrollbar_; 206 bool shouldGoForward = isPinnedRight_ || !hasHorizontalScrollbar_; 207 if (isHorizontalGesture && 208 // For normal pages, canGoBack/canGoForward are checked in the renderer 209 // (ChromeClientImpl::shouldRubberBand()), when it decides if it should 210 // rubberband or send back an event unhandled. The check here is 211 // required for pages with an onmousewheel handler that doesn't call 212 // preventDefault(). 213 ((shouldGoBack && canGoBack && !isRightScroll) || 214 (shouldGoForward && canGoForward && isRightScroll))) { 215 216 // Released by the tracking handler once the gesture is complete. 217 HistoryOverlayController* historyOverlay = 218 [[HistoryOverlayController alloc] 219 initForMode:goForward ? kHistoryOverlayModeForward : 220 kHistoryOverlayModeBack]; 221 222 // The way this API works: gestureAmount is between -1 and 1 (float). If 223 // the user does the gesture for more than about 25% (i.e. < -0.25 or > 224 // 0.25) and then lets go, it is accepted, we get a NSEventPhaseEnded, 225 // and after that the block is called with amounts animating towards 1 226 // (or -1, depending on the direction). If the user lets go below that 227 // threshold, we get NSEventPhaseCancelled, and the amount animates 228 // toward 0. When gestureAmount has reaches its final value, i.e. the 229 // track animation is done, the handler is called with |isComplete| set 230 // to |YES|. 231 // When starting a backwards navigation gesture (swipe from left to right, 232 // gestureAmount will go from 0 to 1), if the user swipes from left to 233 // right and then quickly back to the left, this call can send 234 // NSEventPhaseEnded and then animate to gestureAmount of -1. For a 235 // picture viewer, that makes sense, but for back/forward navigation users 236 // find it confusing. There are two ways to prevent this: 237 // 1. Set Options to NSEventSwipeTrackingLockDirection. This way, 238 // gestureAmount will always stay > 0. 239 // 2. Pass min:0 max:1 (instead of min:-1 max:1). This way, gestureAmount 240 // will become less than 0, but on the quick swipe back to the left, 241 // NSEventPhaseCancelled is sent instead. 242 // The current UI looks nicer with (1) so that swiping the opposite 243 // direction after the initial swipe doesn't cause the shield to move 244 // in the wrong direction. 245 [theEvent trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection 246 dampenAmountThresholdMin:-1 247 max:1 248 usingHandler:^(CGFloat gestureAmount, 249 NSEventPhase phase, 250 BOOL isComplete, 251 BOOL *stop) { 252 if (phase == NSEventPhaseBegan) { 253 [historyOverlay showPanelForView: 254 renderWidgetHost_->GetView()->GetNativeView()]; 255 return; 256 } 257 258 BOOL ended = phase == NSEventPhaseEnded; 259 260 // Dismiss the panel before navigation for immediate visual feedback. 261 [historyOverlay setProgress:gestureAmount]; 262 if (ended) 263 [historyOverlay dismiss]; 264 265 // |gestureAmount| obeys -[NSEvent isDirectionInvertedFromDevice] 266 // automatically. 267 Browser* browser = chrome::FindBrowserWithWindow( 268 historyOverlay.view.window); 269 if (ended && browser) { 270 if (goForward) 271 chrome::GoForward(browser, CURRENT_TAB); 272 else 273 chrome::GoBack(browser, CURRENT_TAB); 274 } 275 276 if (isComplete) 277 [historyOverlay release]; 278 }]; 279 return YES; 280 } 281 } 282 return NO; 283} 284 285- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item 286 isValidItem:(BOOL*)valid { 287 SEL action = [item action]; 288 289 // For now, this action is always enabled for render view; 290 // this is sub-optimal. 291 // TODO(suzhe): Plumb the "can*" methods up from WebCore. 292 if (action == @selector(checkSpelling:)) { 293 *valid = renderWidgetHost_->IsRenderView(); 294 return YES; 295 } 296 297 // TODO(groby): Clarify who sends this and if toggleContinuousSpellChecking: 298 // is still necessary. 299 if (action == @selector(toggleContinuousSpellChecking:)) { 300 if ([(id)item respondsToSelector:@selector(setState:)]) { 301 content::RenderProcessHost* host = renderWidgetHost_->GetProcess(); 302 Profile* profile = Profile::FromBrowserContext(host->GetBrowserContext()); 303 DCHECK(profile); 304 spellcheckChecked_ = 305 profile->GetPrefs()->GetBoolean(prefs::kEnableContinuousSpellcheck); 306 NSCellStateValue checkedState = 307 spellcheckChecked_ ? NSOnState : NSOffState; 308 [(id)item setState:checkedState]; 309 } 310 *valid = spellcheckEnabled_; 311 return YES; 312 } 313 314 return NO; 315} 316 317- (void)compositingIOSurfaceCreated { 318 NSView* nativeView = renderWidgetHost_->GetView()->GetNativeView(); 319 BrowserWindowController* windowController = 320 [BrowserWindowController browserWindowControllerForView:nativeView]; 321 [[windowController overlayableContentsController] 322 activeContentsCompositingIOSurfaceCreated]; 323} 324 325// Spellchecking methods 326// The next five methods are implemented here since this class is the first 327// responder for anything in the browser. 328 329// This message is sent whenever the user specifies that a word should be 330// changed from the spellChecker. 331- (void)changeSpelling:(id)sender { 332 // Grab the currently selected word from the spell panel, as this is the word 333 // that we want to replace the selected word in the text with. 334 NSString* newWord = [[sender selectedCell] stringValue]; 335 if (newWord != nil) { 336 renderWidgetHost_->Replace(base::SysNSStringToUTF16(newWord)); 337 } 338} 339 340// This message is sent by NSSpellChecker whenever the next word should be 341// advanced to, either after a correction or clicking the "Find Next" button. 342// This isn't documented anywhere useful, like in NSSpellProtocol.h with the 343// other spelling panel methods. This is probably because Apple assumes that the 344// the spelling panel will be used with an NSText, which will automatically 345// catch this and advance to the next word for you. Thanks Apple. 346// This is also called from the Edit -> Spelling -> Check Spelling menu item. 347- (void)checkSpelling:(id)sender { 348 renderWidgetHost_->Send(new SpellCheckMsg_AdvanceToNextMisspelling( 349 renderWidgetHost_->GetRoutingID())); 350} 351 352// This message is sent by the spelling panel whenever a word is ignored. 353- (void)ignoreSpelling:(id)sender { 354 // Ideally, we would ask the current RenderView for its tag, but that would 355 // mean making a blocking IPC call from the browser. Instead, 356 // spellcheck_mac::CheckSpelling remembers the last tag and 357 // spellcheck_mac::IgnoreWord assumes that is the correct tag. 358 NSString* wordToIgnore = [sender stringValue]; 359 if (wordToIgnore != nil) 360 spellcheck_mac::IgnoreWord(base::SysNSStringToUTF16(wordToIgnore)); 361} 362 363- (void)showGuessPanel:(id)sender { 364 renderWidgetHost_->Send(new SpellCheckMsg_ToggleSpellPanel( 365 renderWidgetHost_->GetRoutingID(), 366 spellcheck_mac::SpellingPanelVisible())); 367} 368 369- (void)toggleContinuousSpellChecking:(id)sender { 370 content::RenderProcessHost* host = renderWidgetHost_->GetProcess(); 371 Profile* profile = Profile::FromBrowserContext(host->GetBrowserContext()); 372 DCHECK(profile); 373 PrefService* pref = profile->GetPrefs(); 374 pref->SetBoolean(prefs::kEnableContinuousSpellcheck, 375 !pref->GetBoolean(prefs::kEnableContinuousSpellcheck)); 376} 377 378- (void)spellCheckEnabled:(BOOL)enabled checked:(BOOL)checked { 379 spellcheckEnabled_ = enabled; 380 spellcheckChecked_ = checked; 381} 382 383// END Spellchecking methods 384 385@end 386