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