2 * Copyright (C) 2005, 2008, 2010 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
26 #import "WebDynamicScrollBarsViewInternal.h"
28 #import "WebDocument.h"
29 #import "WebFrameInternal.h"
30 #import "WebFrameView.h"
31 #import "WebHTMLViewInternal.h"
32 #import <WebCore/Frame.h>
33 #import <WebCore/FrameView.h>
34 #import <WebKitSystemInterface.h>
36 using namespace WebCore;
38 // FIXME: <rdar://problem/5898985> Mail expects a constant of this name to exist.
39 const int WebCoreScrollbarAlwaysOn = ScrollbarAlwaysOn;
42 // In <rdar://problem/7814899> we saw crashes because WebDynamicScrollBarsView increased in size, breaking ABI compatiblity.
43 COMPILE_ASSERT(sizeof(WebDynamicScrollBarsView) == 0x8c, WebDynamicScrollBarsView_is_expected_size);
46 struct WebDynamicScrollBarsViewPrivate {
47 unsigned inUpdateScrollersLayoutPass;
49 WebCore::ScrollbarMode hScroll;
50 WebCore::ScrollbarMode vScroll;
52 bool hScrollModeLocked;
53 bool vScrollModeLocked;
55 bool suppressScrollers;
56 bool inUpdateScrollers;
57 bool verticallyPinnedByPreviousWheelEvent;
58 bool horizontallyPinnedByPreviousWheelEvent;
60 bool allowsScrollersToOverlapContent;
61 bool alwaysHideHorizontalScroller;
62 bool alwaysHideVerticalScroller;
63 bool horizontalScrollingAllowedButScrollerHidden;
64 bool verticalScrollingAllowedButScrollerHidden;
66 // scrollOrigin is set for various combinations of writing mode and direction.
67 // See the comment next to the corresponding member in ScrollView.h.
70 // Flag to indicate that the scrollbar thumb's initial position needs to
72 bool scrollOriginChanged;
73 NSPoint scrollPositionExcludingOrigin;
75 bool inProgrammaticScroll;
78 @implementation WebDynamicScrollBarsView
80 - (id)initWithFrame:(NSRect)frame
82 if (!(self = [super initWithFrame:frame]))
85 _private = new WebDynamicScrollBarsViewPrivate;
86 memset(_private, 0, sizeof(WebDynamicScrollBarsViewPrivate));
90 - (id)initWithCoder:(NSCoder *)aDecoder
92 if (!(self = [super initWithCoder:aDecoder]))
95 _private = new WebDynamicScrollBarsViewPrivate;
96 memset(_private, 0, sizeof(WebDynamicScrollBarsViewPrivate));
112 - (void)setAllowsHorizontalScrolling:(BOOL)flag
114 if (_private->hScrollModeLocked)
116 if (flag && _private->hScroll == ScrollbarAlwaysOff)
117 _private->hScroll = ScrollbarAuto;
118 else if (!flag && _private->hScroll != ScrollbarAlwaysOff)
119 _private->hScroll = ScrollbarAlwaysOff;
120 [self updateScrollers];
123 - (void)setAllowsScrollersToOverlapContent:(BOOL)flag
125 if (_private->allowsScrollersToOverlapContent == flag)
128 _private->allowsScrollersToOverlapContent = flag;
130 [[self contentView] setFrame:[self contentViewFrame]];
131 [[self documentView] setNeedsLayout:YES];
132 [[self documentView] layout];
135 - (void)setAlwaysHideHorizontalScroller:(BOOL)shouldBeHidden
137 if (_private->alwaysHideHorizontalScroller == shouldBeHidden)
140 _private->alwaysHideHorizontalScroller = shouldBeHidden;
141 [self updateScrollers];
144 - (void)setAlwaysHideVerticalScroller:(BOOL)shouldBeHidden
146 if (_private->alwaysHideVerticalScroller == shouldBeHidden)
149 _private->alwaysHideVerticalScroller = shouldBeHidden;
150 [self updateScrollers];
153 - (BOOL)horizontalScrollingAllowed
155 return _private->horizontalScrollingAllowedButScrollerHidden || [self hasHorizontalScroller];
158 - (BOOL)verticalScrollingAllowed
160 return _private->verticalScrollingAllowedButScrollerHidden || [self hasVerticalScroller];
165 @implementation WebDynamicScrollBarsView (WebInternal)
167 - (NSRect)contentViewFrame
169 NSRect frame = [[self contentView] frame];
171 if ([self hasHorizontalScroller])
172 frame.size.height = (_private->allowsScrollersToOverlapContent ? NSMaxY([[self horizontalScroller] frame]) : NSMinY([[self horizontalScroller] frame]));
173 if ([self hasVerticalScroller])
174 frame.size.width = (_private->allowsScrollersToOverlapContent ? NSMaxX([[self verticalScroller] frame]) : NSMinX([[self verticalScroller] frame]));
182 // [super tile] sets the contentView size so that it does not overlap with the scrollers,
183 // we want to re-set the contentView to overlap scrollers before displaying.
184 if (_private->allowsScrollersToOverlapContent)
185 [[self contentView] setFrame:[self contentViewFrame]];
188 - (void)setSuppressLayout:(BOOL)flag
190 _private->suppressLayout = flag;
193 - (void)setScrollBarsSuppressed:(BOOL)suppressed repaintOnUnsuppress:(BOOL)repaint
195 _private->suppressScrollers = suppressed;
198 [[self verticalScroller] setNeedsDisplay:NO];
199 [[self horizontalScroller] setNeedsDisplay:NO];
202 if (!suppressed && repaint)
203 [super reflectScrolledClipView:[self contentView]];
206 - (void)adjustForScrollOriginChange
208 if (!_private->scrollOriginChanged)
211 _private->scrollOriginChanged = false;
213 NSView *documentView = [self documentView];
214 NSRect documentRect = [documentView bounds];
216 // The call to [NSView scrollPoint:] fires off notification the handler for which needs to know that
217 // we're setting the initial scroll position so it doesn't interpret this as a user action and
218 // fire off a JS event.
219 _private->inProgrammaticScroll = true;
220 [documentView scrollPoint:NSMakePoint(_private->scrollPositionExcludingOrigin.x + documentRect.origin.x, _private->scrollPositionExcludingOrigin.y + documentRect.origin.y)];
221 _private->inProgrammaticScroll = false;
224 static const unsigned cMaxUpdateScrollbarsPass = 2;
226 - (void)updateScrollers
228 NSView *documentView = [self documentView];
230 // If we came in here with the view already needing a layout, then go ahead and do that
231 // first. (This will be the common case, e.g., when the page changes due to window resizing for example).
232 // This layout will not re-enter updateScrollers and does not count towards our max layout pass total.
233 if (!_private->suppressLayout && !_private->suppressScrollers && [documentView isKindOfClass:[WebHTMLView class]]) {
234 WebHTMLView* htmlView = (WebHTMLView*)documentView;
235 if ([htmlView _needsLayout]) {
236 _private->inUpdateScrollers = YES;
237 [(id <WebDocumentView>)documentView layout];
238 _private->inUpdateScrollers = NO;
242 BOOL hasHorizontalScroller = [self hasHorizontalScroller];
243 BOOL hasVerticalScroller = [self hasVerticalScroller];
245 BOOL newHasHorizontalScroller = hasHorizontalScroller;
246 BOOL newHasVerticalScroller = hasVerticalScroller;
249 newHasHorizontalScroller = NO;
250 newHasVerticalScroller = NO;
253 if (_private->hScroll != ScrollbarAuto)
254 newHasHorizontalScroller = (_private->hScroll == ScrollbarAlwaysOn);
255 if (_private->vScroll != ScrollbarAuto)
256 newHasVerticalScroller = (_private->vScroll == ScrollbarAlwaysOn);
258 if (!documentView || _private->suppressLayout || _private->suppressScrollers || (_private->hScroll != ScrollbarAuto && _private->vScroll != ScrollbarAuto)) {
259 _private->horizontalScrollingAllowedButScrollerHidden = newHasHorizontalScroller && _private->alwaysHideHorizontalScroller;
260 if (_private->horizontalScrollingAllowedButScrollerHidden)
261 newHasHorizontalScroller = NO;
263 _private->verticalScrollingAllowedButScrollerHidden = newHasVerticalScroller && _private->alwaysHideVerticalScroller;
264 if (_private->verticalScrollingAllowedButScrollerHidden)
265 newHasVerticalScroller = NO;
267 _private->inUpdateScrollers = YES;
268 if (hasHorizontalScroller != newHasHorizontalScroller)
269 [self setHasHorizontalScroller:newHasHorizontalScroller];
270 if (hasVerticalScroller != newHasVerticalScroller)
271 [self setHasVerticalScroller:newHasVerticalScroller];
272 if (_private->suppressScrollers) {
273 [[self verticalScroller] setNeedsDisplay:NO];
274 [[self horizontalScroller] setNeedsDisplay:NO];
276 _private->inUpdateScrollers = NO;
280 BOOL needsLayout = NO;
282 NSSize documentSize = [documentView frame].size;
283 NSSize visibleSize = [self documentVisibleRect].size;
284 NSSize frameSize = [self frame].size;
286 // When in HiDPI with a scale factor > 1, the visibleSize and frameSize may be non-integral values,
287 // while the documentSize (set by WebCore) will be integral. Round up the non-integral sizes so that
288 // the mismatch won't cause unwanted scrollbars to appear. This can result in slightly cut off content,
289 // but it will always be less than one pixel, which should not be noticeable.
290 visibleSize.width = ceilf(visibleSize.width);
291 visibleSize.height = ceilf(visibleSize.height);
292 frameSize.width = ceilf(frameSize.width);
293 frameSize.height = ceilf(frameSize.height);
295 if (_private->hScroll == ScrollbarAuto) {
296 newHasHorizontalScroller = documentSize.width > visibleSize.width;
297 if (newHasHorizontalScroller && !_private->inUpdateScrollersLayoutPass && documentSize.height <= frameSize.height && documentSize.width <= frameSize.width)
298 newHasHorizontalScroller = NO;
301 if (_private->vScroll == ScrollbarAuto) {
302 newHasVerticalScroller = documentSize.height > visibleSize.height;
303 if (newHasVerticalScroller && !_private->inUpdateScrollersLayoutPass && documentSize.height <= frameSize.height && documentSize.width <= frameSize.width)
304 newHasVerticalScroller = NO;
307 // Unless in ScrollbarsAlwaysOn mode, if we ever turn one scrollbar off, always turn the other one off too.
308 // Never ever try to both gain/lose a scrollbar in the same pass.
309 if (!newHasHorizontalScroller && hasHorizontalScroller && _private->vScroll != ScrollbarAlwaysOn)
310 newHasVerticalScroller = NO;
311 if (!newHasVerticalScroller && hasVerticalScroller && _private->hScroll != ScrollbarAlwaysOn)
312 newHasHorizontalScroller = NO;
314 _private->horizontalScrollingAllowedButScrollerHidden = newHasHorizontalScroller && _private->alwaysHideHorizontalScroller;
315 if (_private->horizontalScrollingAllowedButScrollerHidden)
316 newHasHorizontalScroller = NO;
318 _private->verticalScrollingAllowedButScrollerHidden = newHasVerticalScroller && _private->alwaysHideVerticalScroller;
319 if (_private->verticalScrollingAllowedButScrollerHidden)
320 newHasVerticalScroller = NO;
322 if (hasHorizontalScroller != newHasHorizontalScroller) {
323 _private->inUpdateScrollers = YES;
324 [self setHasHorizontalScroller:newHasHorizontalScroller];
325 _private->inUpdateScrollers = NO;
327 NSView *documentView = [self documentView];
328 NSRect documentRect = [documentView bounds];
329 if (documentRect.origin.y < 0 && !newHasHorizontalScroller)
330 [documentView setBoundsOrigin:NSMakePoint(documentRect.origin.x, documentRect.origin.y + 15)];
333 if (hasVerticalScroller != newHasVerticalScroller) {
334 _private->inUpdateScrollers = YES;
335 [self setHasVerticalScroller:newHasVerticalScroller];
336 _private->inUpdateScrollers = NO;
338 NSView *documentView = [self documentView];
339 NSRect documentRect = [documentView bounds];
340 if (documentRect.origin.x < 0 && !newHasVerticalScroller)
341 [documentView setBoundsOrigin:NSMakePoint(documentRect.origin.x + 15, documentRect.origin.y)];
344 if (needsLayout && _private->inUpdateScrollersLayoutPass < cMaxUpdateScrollbarsPass &&
345 [documentView conformsToProtocol:@protocol(WebDocumentView)]) {
346 _private->inUpdateScrollersLayoutPass++;
347 [(id <WebDocumentView>)documentView setNeedsLayout:YES];
348 [(id <WebDocumentView>)documentView layout];
349 NSSize newDocumentSize = [documentView frame].size;
350 if (NSEqualSizes(documentSize, newDocumentSize)) {
351 // The layout with the new scroll state had no impact on
352 // the document's overall size, so updateScrollers didn't get called.
354 [self updateScrollers];
356 _private->inUpdateScrollersLayoutPass--;
360 // Make the horizontal and vertical scroll bars come and go as needed.
361 - (void)reflectScrolledClipView:(NSClipView *)clipView
363 if (clipView == [self contentView]) {
364 // Prevent appearance of trails because of overlapping views
365 if (_private->allowsScrollersToOverlapContent)
366 [self setDrawsBackground:NO];
368 // FIXME: This hack here prevents infinite recursion that takes place when we
369 // gyrate between having a vertical scroller and not having one. A reproducible
370 // case is clicking on the "the Policy Routing text" link at
371 // http://www.linuxpowered.com/archive/howto/Net-HOWTO-8.html.
372 // The underlying cause is some problem in the NSText machinery, but I was not
373 // able to pin it down.
374 NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
375 if (!_private->inUpdateScrollers && (!currentContext || [currentContext isDrawingToScreen]))
376 [self updateScrollers];
379 // Update the scrollers if they're not being suppressed.
380 if (!_private->suppressScrollers)
381 [super reflectScrolledClipView:clipView];
383 // The call to [NSView reflectScrolledClipView] sets the scrollbar thumb
384 // position to 0 (the left) when the view is initially displayed.
385 // This call updates the initial position correctly.
386 [self adjustForScrollOriginChange];
388 #if USE(ACCELERATED_COMPOSITING) && defined(BUILDING_ON_LEOPARD)
389 NSView *documentView = [self documentView];
390 if ([documentView isKindOfClass:[WebHTMLView class]]) {
391 WebHTMLView *htmlView = (WebHTMLView *)documentView;
392 if ([htmlView _isUsingAcceleratedCompositing])
393 [htmlView _updateLayerHostingViewPosition];
398 - (BOOL)allowsHorizontalScrolling
400 return _private->hScroll != ScrollbarAlwaysOff;
403 - (BOOL)allowsVerticalScrolling
405 return _private->vScroll != ScrollbarAlwaysOff;
408 - (void)scrollingModes:(WebCore::ScrollbarMode*)hMode vertical:(WebCore::ScrollbarMode*)vMode
410 *hMode = _private->hScroll;
411 *vMode = _private->vScroll;
414 - (ScrollbarMode)horizontalScrollingMode
416 return _private->hScroll;
419 - (ScrollbarMode)verticalScrollingMode
421 return _private->vScroll;
424 - (void)setHorizontalScrollingMode:(ScrollbarMode)horizontalMode andLock:(BOOL)lock
426 [self setScrollingModes:horizontalMode vertical:[self verticalScrollingMode] andLock:lock];
429 - (void)setVerticalScrollingMode:(ScrollbarMode)verticalMode andLock:(BOOL)lock
431 [self setScrollingModes:[self horizontalScrollingMode] vertical:verticalMode andLock:lock];
434 // Mail uses this method, so we cannot remove it.
435 - (void)setVerticalScrollingMode:(ScrollbarMode)verticalMode
437 [self setScrollingModes:[self horizontalScrollingMode] vertical:verticalMode andLock:NO];
440 - (void)setScrollingModes:(ScrollbarMode)horizontalMode vertical:(ScrollbarMode)verticalMode andLock:(BOOL)lock
443 if (verticalMode != _private->vScroll && !_private->vScrollModeLocked) {
444 _private->vScroll = verticalMode;
448 if (horizontalMode != _private->hScroll && !_private->hScrollModeLocked) {
449 _private->hScroll = horizontalMode;
454 [self setScrollingModesLocked:YES];
457 [self updateScrollers];
460 - (void)setHorizontalScrollingModeLocked:(BOOL)locked
462 _private->hScrollModeLocked = locked;
465 - (void)setVerticalScrollingModeLocked:(BOOL)locked
467 _private->vScrollModeLocked = locked;
470 - (void)setScrollingModesLocked:(BOOL)locked
472 _private->hScrollModeLocked = _private->vScrollModeLocked = locked;
475 - (BOOL)horizontalScrollingModeLocked
477 return _private->hScrollModeLocked;
480 - (BOOL)verticalScrollingModeLocked
482 return _private->vScrollModeLocked;
485 - (BOOL)autoforwardsScrollWheelEvents
490 - (void)scrollWheel:(NSEvent *)event
495 WKGetWheelEventDeltas(event, &deltaX, &deltaY, &isContinuous);
497 #if !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD)
498 NSEventPhase momentumPhase = [event momentumPhase];
499 BOOL isLatchingEvent = momentumPhase & NSEventPhaseBegan || momentumPhase & NSEventPhaseStationary;
501 int momentumPhase = WKGetNSEventMomentumPhase(event);
502 BOOL isLatchingEvent = momentumPhase == WKEventPhaseBegan || momentumPhase == WKEventPhaseChanged;
505 if (fabsf(deltaY) > fabsf(deltaX)) {
506 if (![self allowsVerticalScrolling]) {
507 [[self nextResponder] scrollWheel:event];
511 if (isLatchingEvent && !_private->verticallyPinnedByPreviousWheelEvent) {
512 double verticalPosition = [[self verticalScroller] doubleValue];
513 if ((deltaY >= 0.0 && verticalPosition == 0.0) || (deltaY <= 0.0 && verticalPosition == 1.0))
517 if (![self allowsHorizontalScrolling]) {
518 [[self nextResponder] scrollWheel:event];
522 if (isLatchingEvent && !_private->horizontallyPinnedByPreviousWheelEvent) {
523 double horizontalPosition = [[self horizontalScroller] doubleValue];
524 if ((deltaX >= 0.0 && horizontalPosition == 0.0) || (deltaX <= 0.0 && horizontalPosition == 1.0))
529 // Calling super can release the last reference. <rdar://problem/7400263>
530 // Hold a reference so the code following the super call will not crash.
533 [super scrollWheel:event];
535 if (!isLatchingEvent) {
536 double verticalPosition = [[self verticalScroller] doubleValue];
537 double horizontalPosition = [[self horizontalScroller] doubleValue];
539 _private->verticallyPinnedByPreviousWheelEvent = (verticalPosition == 0.0 || verticalPosition == 1.0);
540 _private->horizontallyPinnedByPreviousWheelEvent = (horizontalPosition == 0.0 || horizontalPosition == 1.0);
546 // This object will be the parent of the web area in WK1, so it should not be ignored.
547 - (BOOL)accessibilityIsIgnored
552 - (void)setScrollOrigin:(NSPoint)scrollOrigin updatePositionAtAll:(BOOL)updatePositionAtAll immediately:(BOOL)updatePositionSynchronously
554 // The cross-platform ScrollView call already checked to see if the old/new scroll origins were the same or not
555 // so we don't have to check for equivalence here.
556 _private->scrollOrigin = scrollOrigin;
557 id docView = [self documentView];
559 NSRect visibleRect = [self documentVisibleRect];
561 [docView setBoundsOrigin:NSMakePoint(-scrollOrigin.x, -scrollOrigin.y)];
563 if (updatePositionAtAll)
564 _private->scrollOriginChanged = true;
566 // Maintain our original position in the presence of the new scroll origin.
567 _private->scrollPositionExcludingOrigin = NSMakePoint(visibleRect.origin.x + scrollOrigin.x, visibleRect.origin.y + scrollOrigin.y);
569 if (updatePositionAtAll && updatePositionSynchronously) // Otherwise we'll just let the snap happen when we update for the resize.
570 [self adjustForScrollOriginChange];
573 - (NSPoint)scrollOrigin
575 return _private->scrollOrigin;
578 - (BOOL)inProgrammaticScroll
580 return _private->inProgrammaticScroll;