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