Fix build break
[chromium-blink-merge.git] / chrome / browser / renderer_host / chrome_render_widget_host_view_mac_delegate.mm
blobf3973d20131901a69f04c7faa07216bbf99eb28d
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.
5 #import "chrome/browser/renderer_host/chrome_render_widget_host_view_mac_delegate.h"
7 #include <cmath>
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/history_overlay_controller.h"
18 #import "chrome/browser/ui/cocoa/view_id_util.h"
19 #include "chrome/common/pref_names.h"
20 #include "chrome/common/spellcheck_messages.h"
21 #include "chrome/common/url_constants.h"
22 #include "content/public/browser/render_process_host.h"
23 #include "content/public/browser/render_view_host.h"
24 #include "content/public/browser/render_view_host_observer.h"
25 #include "content/public/browser/render_widget_host.h"
26 #include "content/public/browser/render_widget_host_view.h"
27 #include "content/public/browser/web_contents.h"
29 using content::RenderViewHost;
31 // Declare things that are part of the 10.7 SDK.
32 #if !defined(MAC_OS_X_VERSION_10_7) || \
33     MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
34 enum {
35   NSEventPhaseNone        = 0, // event not associated with a phase.
36   NSEventPhaseBegan       = 0x1 << 0,
37   NSEventPhaseStationary  = 0x1 << 1,
38   NSEventPhaseChanged     = 0x1 << 2,
39   NSEventPhaseEnded       = 0x1 << 3,
40   NSEventPhaseCancelled   = 0x1 << 4,
42 typedef NSUInteger NSEventPhase;
44 enum {
45   NSEventSwipeTrackingLockDirection = 0x1 << 0,
46   NSEventSwipeTrackingClampGestureAmount = 0x1 << 1
48 typedef NSUInteger NSEventSwipeTrackingOptions;
50 @interface NSEvent (LionAPI)
51 + (BOOL)isSwipeTrackingFromScrollEventsEnabled;
53 - (NSEventPhase)phase;
54 - (CGFloat)scrollingDeltaX;
55 - (CGFloat)scrollingDeltaY;
56 - (void)trackSwipeEventWithOptions:(NSEventSwipeTrackingOptions)options
57           dampenAmountThresholdMin:(CGFloat)minDampenThreshold
58                                max:(CGFloat)maxDampenThreshold
59                       usingHandler:(void (^)(CGFloat gestureAmount,
60                                              NSEventPhase phase,
61                                              BOOL isComplete,
62                                              BOOL *stop))trackingHandler;
63 @end
64 #endif  // 10.7
66 @interface ChromeRenderWidgetHostViewMacDelegate ()
67 - (BOOL)maybeHandleHistorySwiping:(NSEvent*)theEvent;
68 - (void)spellCheckEnabled:(BOOL)enabled checked:(BOOL)checked;
69 @end
71 namespace ChromeRenderWidgetHostViewMacDelegateInternal {
73 // Filters the message sent to RenderViewHost to know if spellchecking is
74 // enabled or not for the currently focused element.
75 class SpellCheckRenderViewObserver : public content::RenderViewHostObserver {
76  public:
77   SpellCheckRenderViewObserver(
78       RenderViewHost* host,
79       ChromeRenderWidgetHostViewMacDelegate* view_delegate)
80       : content::RenderViewHostObserver(host),
81         view_delegate_(view_delegate) {
82   }
84   virtual ~SpellCheckRenderViewObserver() {
85   }
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   }
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   }
106   void OnToggleSpellCheck(bool enabled, bool checked) {
107     [view_delegate_ spellCheckEnabled:enabled checked:checked];
108   }
110   ChromeRenderWidgetHostViewMacDelegate* view_delegate_;
113 }  // namespace ChromeRenderWidgetHostViewMacDelegateInternal
115 @implementation ChromeRenderWidgetHostViewMacDelegate
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);
124     if (renderWidgetHost_->IsRenderView()) {
125       spellingObserver_.reset(
126           new ChromeRenderWidgetHostViewMacDelegateInternal::
127               SpellCheckRenderViewObserver(
128                   RenderViewHost::From(renderWidgetHost_), self));
129     }
130   }
131   return self;
134 - (void)viewGone:(NSView*)view {
135   view_id_util::UnsetID(view);
136   [self autorelease];
139 - (BOOL)handleEvent:(NSEvent*)event {
140   if ([event type] == NSScrollWheel)
141     return [self maybeHandleHistorySwiping:event];
143   return NO;
146 - (void)gotUnhandledWheelEvent {
147   gotUnhandledWheelEvent_ = YES;
150 - (void)scrollOffsetPinnedToLeft:(BOOL)left toRight:(BOOL)right {
151   isPinnedLeft_ = left;
152   isPinnedRight_ = right;
155 - (void)setHasHorizontalScrollbar:(BOOL)hasHorizontalScrollbar {
156   hasHorizontalScrollbar_ = hasHorizontalScrollbar;
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;
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   }
174   if (!renderWidgetHost_ || !renderWidgetHost_->IsRenderView())
175     return NO;
176   if (DevToolsWindow::IsDevToolsWindow(
177           RenderViewHost::From(renderWidgetHost_))) {
178     return NO;
179   }
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];
188     bool isHorizontalGesture =
189       std::abs(totalScrollDelta_.width) > std::abs(totalScrollDelta_.height);
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     }
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))) {
214       // Released by the tracking handler once the gesture is complete.
215       HistoryOverlayController* historyOverlay =
216           [[HistoryOverlayController alloc]
217               initForMode:goForward ? kHistoryOverlayModeForward :
218                                       kHistoryOverlayModeBack];
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           }
256           BOOL ended = phase == NSEventPhaseEnded;
258           // Dismiss the panel before navigation for immediate visual feedback.
259           [historyOverlay setProgress:gestureAmount];
260           if (ended)
261             [historyOverlay dismiss];
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           }
274           if (isComplete)
275             [historyOverlay release];
276         }];
277       return YES;
278     }
279   }
280   return NO;
283 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
284                       isValidItem:(BOOL*)valid {
285   SEL action = [item action];
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   }
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   }
312   return NO;
315 // Spellchecking methods
316 // The next five methods are implemented here since this class is the first
317 // responder for anything in the browser.
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   }
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()));
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));
353 - (void)showGuessPanel:(id)sender {
354   renderWidgetHost_->Send(new SpellCheckMsg_ToggleSpellPanel(
355       renderWidgetHost_->GetRoutingID(),
356       spellcheck_mac::SpellingPanelVisible()));
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));
368 - (void)spellCheckEnabled:(BOOL)enabled checked:(BOOL)checked {
369   spellcheckEnabled_ = enabled;
370   spellcheckChecked_ = checked;
373 // END Spellchecking methods
375 @end