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