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