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