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