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