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