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