chrome_render_widget_host_view_mac_delegate.mm revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
180bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru// Copyright (c) 2012 The Chromium Authors. All rights reserved. 280bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru// Use of this source code is governed by a BSD-style license that can be 380bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru// found in the LICENSE file. 480bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru 580bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#import "chrome/browser/renderer_host/chrome_render_widget_host_view_mac_delegate.h" 680bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru 780bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include <cmath> 880bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru 9096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger#include "base/prefs/pref_service.h" 1080bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "base/sys_string_conversions.h" 1180bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "chrome/browser/devtools/devtools_window.h" 12096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger#include "chrome/browser/profiles/profile.h" 1380bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "chrome/browser/spellchecker/spellcheck_platform_mac.h" 1480bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "chrome/browser/ui/browser.h" 1580bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "chrome/browser/ui/browser_commands.h" 16096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger#include "chrome/browser/ui/browser_finder.h" 1780bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#import "chrome/browser/ui/cocoa/history_overlay_controller.h" 1880bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#import "chrome/browser/ui/cocoa/view_id_util.h" 1980bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "chrome/common/pref_names.h" 2080bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "chrome/common/spellcheck_messages.h" 2180bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "chrome/common/url_constants.h" 2280bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "content/public/browser/render_process_host.h" 2380bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "content/public/browser/render_view_host.h" 2480bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "content/public/browser/render_view_host_observer.h" 2580bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "content/public/browser/render_widget_host.h" 2680bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "content/public/browser/render_widget_host_view.h" 2780bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#include "content/public/browser/web_contents.h" 2880bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru 2980bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queruusing content::RenderViewHost; 30096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger 31096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger// Declare things that are part of the 10.7 SDK. 3280bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#if !defined(MAC_OS_X_VERSION_10_7) || \ 3380bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7 3480bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queruenum { 3580bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru NSEventPhaseNone = 0, // event not associated with a phase. 36096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger NSEventPhaseBegan = 0x1 << 0, 3780bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru NSEventPhaseStationary = 0x1 << 1, 38096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger NSEventPhaseChanged = 0x1 << 2, 39096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger NSEventPhaseEnded = 0x1 << 3, 40096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger NSEventPhaseCancelled = 0x1 << 4, 41096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger}; 420a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenbergertypedef NSUInteger NSEventPhase; 43096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger 4480bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queruenum { 45096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger NSEventSwipeTrackingLockDirection = 0x1 << 0, 46096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger NSEventSwipeTrackingClampGestureAmount = 0x1 << 1 47096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger}; 48096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenbergertypedef NSUInteger NSEventSwipeTrackingOptions; 49096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger 50096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger@interface NSEvent (LionAPI) 51096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger+ (BOOL)isSwipeTrackingFromScrollEventsEnabled; 5280bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru 5380bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru- (NSEventPhase)phase; 54096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger- (CGFloat)scrollingDeltaX; 55096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger- (CGFloat)scrollingDeltaY; 56096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger- (void)trackSwipeEventWithOptions:(NSEventSwipeTrackingOptions)options 57096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger dampenAmountThresholdMin:(CGFloat)minDampenThreshold 5880bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru max:(CGFloat)maxDampenThreshold 59096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger usingHandler:(void (^)(CGFloat gestureAmount, 6080bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru NSEventPhase phase, 6180bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru BOOL isComplete, 6280bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru BOOL *stop))trackingHandler; 63096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger@end 6480bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru#endif // 10.7 6580bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru 6680bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru@interface ChromeRenderWidgetHostViewMacDelegate () 6780bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru- (BOOL)maybeHandleHistorySwiping:(NSEvent*)theEvent; 6880bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru- (void)spellCheckEnabled:(BOOL)enabled checked:(BOOL)checked; 6980bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru@end 7080bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru 7180bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Querunamespace ChromeRenderWidgetHostViewMacDelegateInternal { 7280bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru 7380bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru// Filters the message sent to RenderViewHost to know if spellchecking is 7480bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru// enabled or not for the currently focused element. 7580bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queruclass SpellCheckRenderViewObserver : public content::RenderViewHostObserver { 76096defe64d408e54474fe19f418c95bf1a554fc7Derek Sollenberger public: 7780bacfeb4bda06541e8695bd502229727bccfeaJean-Baptiste Queru SpellCheckRenderViewObserver( 780a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger RenderViewHost* host, 790a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger ChromeRenderWidgetHostViewMacDelegate* view_delegate) 800a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger : content::RenderViewHostObserver(host), 810a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger 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 = chrome::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 = chrome::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 // TODO(groby): Clarify who sends this and if toggleContinuousSpellChecking: 296 // is still necessary. 297 if (action == @selector(toggleContinuousSpellChecking:)) { 298 if ([(id)item respondsToSelector:@selector(setState:)]) { 299 content::RenderProcessHost* host = renderWidgetHost_->GetProcess(); 300 Profile* profile = Profile::FromBrowserContext(host->GetBrowserContext()); 301 DCHECK(profile); 302 spellcheckChecked_ = 303 profile->GetPrefs()->GetBoolean(prefs::kEnableContinuousSpellcheck); 304 NSCellStateValue checkedState = 305 spellcheckChecked_ ? NSOnState : NSOffState; 306 [(id)item setState:checkedState]; 307 } 308 *valid = spellcheckEnabled_; 309 return YES; 310 } 311 312 return NO; 313} 314 315// Spellchecking methods 316// The next five methods are implemented here since this class is the first 317// responder for anything in the browser. 318 319// This message is sent whenever the user specifies that a word should be 320// changed from the spellChecker. 321- (void)changeSpelling:(id)sender { 322 // Grab the currently selected word from the spell panel, as this is the word 323 // that we want to replace the selected word in the text with. 324 NSString* newWord = [[sender selectedCell] stringValue]; 325 if (newWord != nil) { 326 renderWidgetHost_->Replace(base::SysNSStringToUTF16(newWord)); 327 } 328} 329 330// This message is sent by NSSpellChecker whenever the next word should be 331// advanced to, either after a correction or clicking the "Find Next" button. 332// This isn't documented anywhere useful, like in NSSpellProtocol.h with the 333// other spelling panel methods. This is probably because Apple assumes that the 334// the spelling panel will be used with an NSText, which will automatically 335// catch this and advance to the next word for you. Thanks Apple. 336// This is also called from the Edit -> Spelling -> Check Spelling menu item. 337- (void)checkSpelling:(id)sender { 338 renderWidgetHost_->Send(new SpellCheckMsg_AdvanceToNextMisspelling( 339 renderWidgetHost_->GetRoutingID())); 340} 341 342// This message is sent by the spelling panel whenever a word is ignored. 343- (void)ignoreSpelling:(id)sender { 344 // Ideally, we would ask the current RenderView for its tag, but that would 345 // mean making a blocking IPC call from the browser. Instead, 346 // spellcheck_mac::CheckSpelling remembers the last tag and 347 // spellcheck_mac::IgnoreWord assumes that is the correct tag. 348 NSString* wordToIgnore = [sender stringValue]; 349 if (wordToIgnore != nil) 350 spellcheck_mac::IgnoreWord(base::SysNSStringToUTF16(wordToIgnore)); 351} 352 353- (void)showGuessPanel:(id)sender { 354 renderWidgetHost_->Send(new SpellCheckMsg_ToggleSpellPanel( 355 renderWidgetHost_->GetRoutingID(), 356 spellcheck_mac::SpellingPanelVisible())); 357} 358 359- (void)toggleContinuousSpellChecking:(id)sender { 360 content::RenderProcessHost* host = renderWidgetHost_->GetProcess(); 361 Profile* profile = Profile::FromBrowserContext(host->GetBrowserContext()); 362 DCHECK(profile); 363 PrefService* pref = profile->GetPrefs(); 364 pref->SetBoolean(prefs::kEnableContinuousSpellcheck, 365 !pref->GetBoolean(prefs::kEnableContinuousSpellcheck)); 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