WebKit/mac/WebView/WebDynamicScrollBarsView.mm
changeset 0 4f2f89ce4247
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebKit/mac/WebView/WebDynamicScrollBarsView.mm	Fri Sep 17 09:02:29 2010 +0300
@@ -0,0 +1,528 @@
+/*
+ * Copyright (C) 2005, 2008, 2010 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "WebDynamicScrollBarsViewInternal.h"
+
+#import "WebDocument.h"
+#import "WebFrameInternal.h"
+#import "WebFrameView.h"
+#import "WebHTMLViewInternal.h"
+#import <WebCore/Frame.h>
+#import <WebCore/FrameView.h>
+#import <WebKitSystemInterface.h>
+
+using namespace WebCore;
+
+// FIXME: <rdar://problem/5898985> Mail expects a constant of this name to exist.
+const int WebCoreScrollbarAlwaysOn = ScrollbarAlwaysOn;
+
+#ifndef __OBJC2__
+// In <rdar://problem/7814899> we saw crashes because WebDynamicScrollBarsView increased in size, breaking ABI compatiblity.
+COMPILE_ASSERT(sizeof(WebDynamicScrollBarsView) == 0x8c, WebDynamicScrollBarsView_is_expected_size);
+#endif
+
+struct WebDynamicScrollBarsViewPrivate {
+    unsigned inUpdateScrollersLayoutPass;
+
+    WebCore::ScrollbarMode hScroll;
+    WebCore::ScrollbarMode vScroll;
+
+    bool hScrollModeLocked;
+    bool vScrollModeLocked;
+    bool suppressLayout;
+    bool suppressScrollers;
+    bool inUpdateScrollers;
+    bool verticallyPinnedByPreviousWheelEvent;
+    bool horizontallyPinnedByPreviousWheelEvent;
+
+    bool allowsScrollersToOverlapContent;
+    bool alwaysHideHorizontalScroller;
+    bool alwaysHideVerticalScroller;
+    bool horizontalScrollingAllowedButScrollerHidden;
+    bool verticalScrollingAllowedButScrollerHidden;
+};
+
+@implementation WebDynamicScrollBarsView
+
+- (id)initWithFrame:(NSRect)frame
+{
+    if (!(self = [super initWithFrame:frame]))
+        return nil;
+
+    _private = new WebDynamicScrollBarsViewPrivate;
+    memset(_private, 0, sizeof(WebDynamicScrollBarsViewPrivate));
+    return self;
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder
+{
+    if (!(self = [super initWithCoder:aDecoder]))
+        return nil;
+
+    _private = new WebDynamicScrollBarsViewPrivate;
+    memset(_private, 0, sizeof(WebDynamicScrollBarsViewPrivate));
+    return self;
+}
+
+- (void)dealloc
+{
+    delete _private;
+    [super dealloc];
+}
+
+- (void)finalize
+{
+    delete _private;
+    [super finalize];
+}
+
+- (void)setAllowsHorizontalScrolling:(BOOL)flag
+{
+    if (_private->hScrollModeLocked)
+        return;
+    if (flag && _private->hScroll == ScrollbarAlwaysOff)
+        _private->hScroll = ScrollbarAuto;
+    else if (!flag && _private->hScroll != ScrollbarAlwaysOff)
+        _private->hScroll = ScrollbarAlwaysOff;
+    [self updateScrollers];
+}
+
+- (void)setAllowsScrollersToOverlapContent:(BOOL)flag
+{
+    if (_private->allowsScrollersToOverlapContent == flag)
+        return;
+
+    _private->allowsScrollersToOverlapContent = flag;
+
+    [[self contentView] setFrame:[self contentViewFrame]];
+    [[self documentView] setNeedsLayout:YES];
+    [[self documentView] layout];
+}
+
+- (void)setAlwaysHideHorizontalScroller:(BOOL)shouldBeHidden
+{
+    if (_private->alwaysHideHorizontalScroller == shouldBeHidden)
+        return;
+
+    _private->alwaysHideHorizontalScroller = shouldBeHidden;
+    [self updateScrollers];
+}
+
+- (void)setAlwaysHideVerticalScroller:(BOOL)shouldBeHidden
+{
+    if (_private->alwaysHideVerticalScroller == shouldBeHidden)
+        return;
+
+    _private->alwaysHideVerticalScroller = shouldBeHidden;
+    [self updateScrollers];
+}
+
+- (BOOL)horizontalScrollingAllowed
+{
+    return _private->horizontalScrollingAllowedButScrollerHidden || [self hasHorizontalScroller];
+}
+
+- (BOOL)verticalScrollingAllowed
+{
+    return _private->verticalScrollingAllowedButScrollerHidden || [self hasVerticalScroller];
+}
+
+@end
+
+@implementation WebDynamicScrollBarsView (WebInternal)
+
+- (NSRect)contentViewFrame
+{
+    NSRect frame = [[self contentView] frame];
+
+    if ([self hasHorizontalScroller])
+        frame.size.height = (_private->allowsScrollersToOverlapContent ? NSMaxY([[self horizontalScroller] frame]) : NSMinY([[self horizontalScroller] frame]));
+    if ([self hasVerticalScroller])
+        frame.size.width = (_private->allowsScrollersToOverlapContent ? NSMaxX([[self verticalScroller] frame]) : NSMinX([[self verticalScroller] frame]));
+    return frame;
+}
+
+- (void)tile
+{
+    [super tile];
+
+    // [super tile] sets the contentView size so that it does not overlap with the scrollers,
+    // we want to re-set the contentView to overlap scrollers before displaying.
+    if (_private->allowsScrollersToOverlapContent)
+        [[self contentView] setFrame:[self contentViewFrame]];
+}
+
+- (void)setSuppressLayout:(BOOL)flag
+{
+    _private->suppressLayout = flag;
+}
+
+- (void)setScrollBarsSuppressed:(BOOL)suppressed repaintOnUnsuppress:(BOOL)repaint
+{
+    _private->suppressScrollers = suppressed;
+
+    // This code was originally changes for a Leopard performance imporvement. We decided to 
+    // ifdef it to fix correctness issues on Tiger documented in <rdar://problem/5441823>.
+#ifndef BUILDING_ON_TIGER
+    if (suppressed) {
+        [[self verticalScroller] setNeedsDisplay:NO];
+        [[self horizontalScroller] setNeedsDisplay:NO];
+    }
+
+    if (!suppressed && repaint)
+        [super reflectScrolledClipView:[self contentView]];
+#else
+    if (suppressed || repaint) {
+        [[self verticalScroller] setNeedsDisplay:!suppressed];
+        [[self horizontalScroller] setNeedsDisplay:!suppressed];
+    }
+#endif
+}
+
+static const unsigned cMaxUpdateScrollbarsPass = 2;
+
+- (void)updateScrollers
+{
+    NSView *documentView = [self documentView];
+
+    // If we came in here with the view already needing a layout, then go ahead and do that
+    // first.  (This will be the common case, e.g., when the page changes due to window resizing for example).
+    // This layout will not re-enter updateScrollers and does not count towards our max layout pass total.
+    if (!_private->suppressLayout && !_private->suppressScrollers && [documentView isKindOfClass:[WebHTMLView class]]) {
+        WebHTMLView* htmlView = (WebHTMLView*)documentView;
+        if ([htmlView _needsLayout]) {
+            _private->inUpdateScrollers = YES;
+            [(id <WebDocumentView>)documentView layout];
+            _private->inUpdateScrollers = NO;
+        }
+    }
+
+    BOOL hasHorizontalScroller = [self hasHorizontalScroller];
+    BOOL hasVerticalScroller = [self hasVerticalScroller];
+
+    BOOL newHasHorizontalScroller = hasHorizontalScroller;
+    BOOL newHasVerticalScroller = hasVerticalScroller;
+
+    if (!documentView) {
+        newHasHorizontalScroller = NO;
+        newHasVerticalScroller = NO;
+    }
+
+    if (_private->hScroll != ScrollbarAuto)
+        newHasHorizontalScroller = (_private->hScroll == ScrollbarAlwaysOn);
+    if (_private->vScroll != ScrollbarAuto)
+        newHasVerticalScroller = (_private->vScroll == ScrollbarAlwaysOn);
+
+    if (!documentView || _private->suppressLayout || _private->suppressScrollers || (_private->hScroll != ScrollbarAuto && _private->vScroll != ScrollbarAuto)) {
+        _private->horizontalScrollingAllowedButScrollerHidden = newHasHorizontalScroller && _private->alwaysHideHorizontalScroller;
+        if (_private->horizontalScrollingAllowedButScrollerHidden)
+            newHasHorizontalScroller = NO;
+
+        _private->verticalScrollingAllowedButScrollerHidden = newHasVerticalScroller && _private->alwaysHideVerticalScroller;
+        if (_private->verticalScrollingAllowedButScrollerHidden)
+            newHasVerticalScroller = NO;
+
+        _private->inUpdateScrollers = YES;
+        if (hasHorizontalScroller != newHasHorizontalScroller)
+            [self setHasHorizontalScroller:newHasHorizontalScroller];
+        if (hasVerticalScroller != newHasVerticalScroller)
+            [self setHasVerticalScroller:newHasVerticalScroller];
+        if (_private->suppressScrollers) {
+            [[self verticalScroller] setNeedsDisplay:NO];
+            [[self horizontalScroller] setNeedsDisplay:NO];
+        }
+        _private->inUpdateScrollers = NO;
+        return;
+    }
+
+    BOOL needsLayout = NO;
+
+    NSSize documentSize = [documentView frame].size;
+    NSSize visibleSize = [self documentVisibleRect].size;
+    NSSize frameSize = [self frame].size;
+    
+    // When in HiDPI with a scale factor > 1, the visibleSize and frameSize may be non-integral values,
+    // while the documentSize (set by WebCore) will be integral.  Round up the non-integral sizes so that
+    // the mismatch won't cause unwanted scrollbars to appear.  This can result in slightly cut off content,
+    // but it will always be less than one pixel, which should not be noticeable.
+    visibleSize.width = ceilf(visibleSize.width);
+    visibleSize.height = ceilf(visibleSize.height);
+    frameSize.width = ceilf(frameSize.width);
+    frameSize.height = ceilf(frameSize.height);
+
+    if (_private->hScroll == ScrollbarAuto) {
+        newHasHorizontalScroller = documentSize.width > visibleSize.width;
+        if (newHasHorizontalScroller && !_private->inUpdateScrollersLayoutPass && documentSize.height <= frameSize.height && documentSize.width <= frameSize.width)
+            newHasHorizontalScroller = NO;
+    }
+
+    if (_private->vScroll == ScrollbarAuto) {
+        newHasVerticalScroller = documentSize.height > visibleSize.height;
+        if (newHasVerticalScroller && !_private->inUpdateScrollersLayoutPass && documentSize.height <= frameSize.height && documentSize.width <= frameSize.width)
+            newHasVerticalScroller = NO;
+    }
+
+    // Unless in ScrollbarsAlwaysOn mode, if we ever turn one scrollbar off, always turn the other one off too.
+    // Never ever try to both gain/lose a scrollbar in the same pass.
+    if (!newHasHorizontalScroller && hasHorizontalScroller && _private->vScroll != ScrollbarAlwaysOn)
+        newHasVerticalScroller = NO;
+    if (!newHasVerticalScroller && hasVerticalScroller && _private->hScroll != ScrollbarAlwaysOn)
+        newHasHorizontalScroller = NO;
+
+    _private->horizontalScrollingAllowedButScrollerHidden = newHasHorizontalScroller && _private->alwaysHideHorizontalScroller;
+    if (_private->horizontalScrollingAllowedButScrollerHidden)
+        newHasHorizontalScroller = NO;
+
+    _private->verticalScrollingAllowedButScrollerHidden = newHasVerticalScroller && _private->alwaysHideVerticalScroller;
+    if (_private->verticalScrollingAllowedButScrollerHidden)
+        newHasVerticalScroller = NO;
+
+    if (hasHorizontalScroller != newHasHorizontalScroller) {
+        _private->inUpdateScrollers = YES;
+        [self setHasHorizontalScroller:newHasHorizontalScroller];
+        _private->inUpdateScrollers = NO;
+        needsLayout = YES;
+    }
+
+    if (hasVerticalScroller != newHasVerticalScroller) {
+        _private->inUpdateScrollers = YES;
+        [self setHasVerticalScroller:newHasVerticalScroller];
+        _private->inUpdateScrollers = NO;
+        needsLayout = YES;
+    }
+
+    if (needsLayout && _private->inUpdateScrollersLayoutPass < cMaxUpdateScrollbarsPass &&
+        [documentView conformsToProtocol:@protocol(WebDocumentView)]) {
+        _private->inUpdateScrollersLayoutPass++;
+        [(id <WebDocumentView>)documentView setNeedsLayout:YES];
+        [(id <WebDocumentView>)documentView layout];
+        NSSize newDocumentSize = [documentView frame].size;
+        if (NSEqualSizes(documentSize, newDocumentSize)) {
+            // The layout with the new scroll state had no impact on
+            // the document's overall size, so updateScrollers didn't get called.
+            // Recur manually.
+            [self updateScrollers];
+        }
+        _private->inUpdateScrollersLayoutPass--;
+    }
+}
+
+// Make the horizontal and vertical scroll bars come and go as needed.
+- (void)reflectScrolledClipView:(NSClipView *)clipView
+{
+    if (clipView == [self contentView]) {
+        // Prevent appearance of trails because of overlapping views
+        if (_private->allowsScrollersToOverlapContent)
+            [self setDrawsBackground:NO];
+
+        // FIXME: This hack here prevents infinite recursion that takes place when we
+        // gyrate between having a vertical scroller and not having one. A reproducible
+        // case is clicking on the "the Policy Routing text" link at
+        // http://www.linuxpowered.com/archive/howto/Net-HOWTO-8.html.
+        // The underlying cause is some problem in the NSText machinery, but I was not
+        // able to pin it down.
+        NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
+        if (!_private->inUpdateScrollers && (!currentContext || [currentContext isDrawingToScreen]))
+            [self updateScrollers];
+    }
+
+    // This code was originally changed for a Leopard performance imporvement. We decided to 
+    // ifdef it to fix correctness issues on Tiger documented in <rdar://problem/5441823>.
+#ifndef BUILDING_ON_TIGER
+    // Update the scrollers if they're not being suppressed.
+    if (!_private->suppressScrollers)
+        [super reflectScrolledClipView:clipView];
+#else
+    [super reflectScrolledClipView:clipView];
+
+    // Validate the scrollers if they're being suppressed.
+    if (_private->suppressScrollers) {
+        [[self verticalScroller] setNeedsDisplay:NO];
+        [[self horizontalScroller] setNeedsDisplay:NO];
+    }
+#endif
+
+#if USE(ACCELERATED_COMPOSITING) && defined(BUILDING_ON_LEOPARD)
+    NSView *documentView = [self documentView];
+    if ([documentView isKindOfClass:[WebHTMLView class]]) {
+        WebHTMLView *htmlView = (WebHTMLView *)documentView;
+        if ([htmlView _isUsingAcceleratedCompositing])
+            [htmlView _updateLayerHostingViewPosition];
+    }
+#endif
+}
+
+- (BOOL)allowsHorizontalScrolling
+{
+    return _private->hScroll != ScrollbarAlwaysOff;
+}
+
+- (BOOL)allowsVerticalScrolling
+{
+    return _private->vScroll != ScrollbarAlwaysOff;
+}
+
+- (void)scrollingModes:(WebCore::ScrollbarMode*)hMode vertical:(WebCore::ScrollbarMode*)vMode
+{
+    *hMode = _private->hScroll;
+    *vMode = _private->vScroll;
+}
+
+- (ScrollbarMode)horizontalScrollingMode
+{
+    return _private->hScroll;
+}
+
+- (ScrollbarMode)verticalScrollingMode
+{
+    return _private->vScroll;
+}
+
+- (void)setHorizontalScrollingMode:(ScrollbarMode)horizontalMode andLock:(BOOL)lock
+{
+    [self setScrollingModes:horizontalMode vertical:[self verticalScrollingMode] andLock:lock];
+}
+
+- (void)setVerticalScrollingMode:(ScrollbarMode)verticalMode andLock:(BOOL)lock
+{
+    [self setScrollingModes:[self horizontalScrollingMode] vertical:verticalMode andLock:lock];
+}
+
+// Mail uses this method, so we cannot remove it. 
+- (void)setVerticalScrollingMode:(ScrollbarMode)verticalMode 
+{ 
+    [self setScrollingModes:[self horizontalScrollingMode] vertical:verticalMode andLock:NO]; 
+} 
+
+- (void)setScrollingModes:(ScrollbarMode)horizontalMode vertical:(ScrollbarMode)verticalMode andLock:(BOOL)lock
+{
+    BOOL update = NO;
+    if (verticalMode != _private->vScroll && !_private->vScrollModeLocked) {
+        _private->vScroll = verticalMode;
+        update = YES;
+    }
+
+    if (horizontalMode != _private->hScroll && !_private->hScrollModeLocked) {
+        _private->hScroll = horizontalMode;
+        update = YES;
+    }
+
+    if (lock)
+        [self setScrollingModesLocked:YES];
+
+    if (update)
+        [self updateScrollers];
+}
+
+- (void)setHorizontalScrollingModeLocked:(BOOL)locked
+{
+    _private->hScrollModeLocked = locked;
+}
+
+- (void)setVerticalScrollingModeLocked:(BOOL)locked
+{
+    _private->vScrollModeLocked = locked;
+}
+
+- (void)setScrollingModesLocked:(BOOL)locked
+{
+    _private->hScrollModeLocked = _private->vScrollModeLocked = locked;
+}
+
+- (BOOL)horizontalScrollingModeLocked
+{
+    return _private->hScrollModeLocked;
+}
+
+- (BOOL)verticalScrollingModeLocked
+{
+    return _private->vScrollModeLocked;
+}
+
+- (BOOL)autoforwardsScrollWheelEvents
+{
+    return YES;
+}
+
+- (void)scrollWheel:(NSEvent *)event
+{
+    float deltaX;
+    float deltaY;
+    BOOL isContinuous;
+    WKGetWheelEventDeltas(event, &deltaX, &deltaY, &isContinuous);
+
+    BOOL isLatchingEvent = WKIsLatchingWheelEvent(event);
+
+    if (fabsf(deltaY) > fabsf(deltaX)) {
+        if (![self allowsVerticalScrolling]) {
+            [[self nextResponder] scrollWheel:event];
+            return;
+        }
+
+        if (isLatchingEvent && !_private->verticallyPinnedByPreviousWheelEvent) {
+            double verticalPosition = [[self verticalScroller] doubleValue];
+            if ((deltaY >= 0.0 && verticalPosition == 0.0) || (deltaY <= 0.0 && verticalPosition == 1.0))
+                return;
+        }
+    } else {
+        if (![self allowsHorizontalScrolling]) {
+            [[self nextResponder] scrollWheel:event];
+            return;
+        }
+
+        if (isLatchingEvent && !_private->horizontallyPinnedByPreviousWheelEvent) {
+            double horizontalPosition = [[self horizontalScroller] doubleValue];
+            if ((deltaX >= 0.0 && horizontalPosition == 0.0) || (deltaX <= 0.0 && horizontalPosition == 1.0))
+                return;
+        }
+    }
+
+    // Calling super can release the last reference. <rdar://problem/7400263>
+    // Hold a reference so the code following the super call will not crash.
+    [self retain];
+
+    [super scrollWheel:event];
+
+    if (!isLatchingEvent) {
+        double verticalPosition = [[self verticalScroller] doubleValue];
+        double horizontalPosition = [[self horizontalScroller] doubleValue];
+
+        _private->verticallyPinnedByPreviousWheelEvent = (verticalPosition == 0.0 || verticalPosition == 1.0);
+        _private->horizontallyPinnedByPreviousWheelEvent = (horizontalPosition == 0.0 || horizontalPosition == 1.0);
+    }
+
+    [self release];
+}
+
+- (BOOL)accessibilityIsIgnored 
+{
+    id docView = [self documentView];
+    if ([docView isKindOfClass:[WebFrameView class]] && ![(WebFrameView *)docView allowsScrolling])
+        return YES;
+    
+    return [super accessibilityIsIgnored];
+}
+
+@end