WebDynamicScrollBarsView.mm   [plain text]


/*
 * 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/DeprecatedGlobalSettings.h>
#import <WebCore/Frame.h>
#import <WebCore/FrameView.h>
#import <WebCore/PlatformEventFactoryMac.h>

using namespace WebCore;

#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

@interface NSScrollView(WebNSScrollViewDetails)
+ (Class)_horizontalScrollerClass;
+ (Class)_verticalScrollerClass;
@end

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;

    // scrollOrigin is set for various combinations of writing mode and direction.
    // See the comment next to the corresponding member in ScrollView.h.
    NSPoint scrollOrigin;

    // Flag to indicate that the scrollbar thumb's initial position needs to
    // be manually set.
    bool scrollOriginChanged;
    NSPoint scrollPositionExcludingOrigin;

    bool inProgrammaticScroll;
};

@implementation WebDynamicScrollBarsView

static Class customScrollerClass;

+ (Class)_horizontalScrollerClass
{
    if (DeprecatedGlobalSettings::mockScrollbarsEnabled() && customScrollerClass)
        return customScrollerClass;

    return [super _horizontalScrollerClass];
}

+ (Class)_verticalScrollerClass
{
    if (DeprecatedGlobalSettings::mockScrollbarsEnabled() && customScrollerClass)
        return customScrollerClass;

    return [super _horizontalScrollerClass];
}

+ (void)setCustomScrollerClass:(Class)scrollerClass
{
    customScrollerClass = scrollerClass;
}

- (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)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];
}

static BOOL shouldRoundScrollOrigin(WebDynamicScrollBarsView *view)
{
    NSView *documentView = [view documentView];
    if (![documentView isKindOfClass:[WebHTMLView class]])
        return NO;

    Frame* frame = core([(WebHTMLView *)documentView _frame]);
    if (!frame)
        return NO;
    
    FrameView *frameView = frame->view();
    if (!frameView)
        return NO;

    return frameView->hasViewportConstrainedObjects();
}

- (void)scrollClipView:(NSClipView *)clipView toPoint:(NSPoint)point
{
    if (shouldRoundScrollOrigin(self)) {
        // WebCore isn't yet able to handle subpixel scrolling, as can happen on Retina displays. For
        // now we'll round to the nearest pixel. Once subpixel layout is enabled in WebCore we may be
        // able to remove this method entirely.
        point.x = round(point.x);
        point.y = round(point.y);
    }

    [super scrollClipView:clipView toPoint:point];
}

@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;

    if (suppressed) {
        [[self verticalScroller] setNeedsDisplay:NO];
        [[self horizontalScroller] setNeedsDisplay:NO];
    }

    if (!suppressed && repaint)
        [super reflectScrolledClipView:[self contentView]];
}

- (void)adjustForScrollOriginChange
{
    if (!_private->scrollOriginChanged)
        return;

    _private->scrollOriginChanged = false;

    NSView *documentView = [self documentView];
    NSRect documentRect = [documentView bounds];

    // The call to [NSView scrollPoint:] fires off notification the handler for which needs to know that
    // we're setting the initial scroll position so it doesn't interpret this as a user action and
    // fire off a JS event.
    _private->inProgrammaticScroll = true;
    [documentView scrollPoint:NSMakePoint(_private->scrollPositionExcludingOrigin.x + documentRect.origin.x, _private->scrollPositionExcludingOrigin.y + documentRect.origin.y)];
    _private->inProgrammaticScroll = false;
}

static const unsigned cMaxUpdateScrollbarsPass = 2;

- (void)updateScrollers
{
    NSView *documentView = [self documentView];

    // If we came in here with the view already needing a layout, then 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;
        NSView *documentView = [self documentView];
        NSRect documentRect = [documentView bounds];
        if (documentRect.origin.y < 0 && !newHasHorizontalScroller)
            [documentView setBoundsOrigin:NSMakePoint(documentRect.origin.x, documentRect.origin.y + 15)];
    }

    if (hasVerticalScroller != newHasVerticalScroller) {
        _private->inUpdateScrollers = YES;
        [self setHasVerticalScroller:newHasVerticalScroller];
        _private->inUpdateScrollers = NO;
        needsLayout = YES;
        NSView *documentView = [self documentView];
        NSRect documentRect = [documentView bounds];
        if (documentRect.origin.x < 0 && !newHasVerticalScroller)
            [documentView setBoundsOrigin:NSMakePoint(documentRect.origin.x + 15, documentRect.origin.y)];
    }

    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];
    }

    // Update the scrollers if they're not being suppressed.
    if (!_private->suppressScrollers)
        [super reflectScrolledClipView:clipView];

    // The call to [NSView reflectScrolledClipView] sets the scrollbar thumb
    // position to 0 (the left) when the view is initially displayed.
    // This call updates the initial position correctly.
    [self adjustForScrollOriginChange];
}

- (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;
    getWheelEventDeltas(event, deltaX, deltaY, isContinuous);

    NSEventPhase momentumPhase = [event momentumPhase];
    BOOL isLatchingEvent = momentumPhase & NSEventPhaseBegan || momentumPhase & NSEventPhaseStationary;

    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];
}

// This object will be the parent of the web area in WK1, so it should not be ignored.
- (BOOL)accessibilityIsIgnored 
{
    return NO;
}

- (void)setScrollOrigin:(NSPoint)scrollOrigin updatePositionAtAll:(BOOL)updatePositionAtAll immediately:(BOOL)updatePositionSynchronously
{
    // The cross-platform ScrollView call already checked to see if the old/new scroll origins were the same or not
    // so we don't have to check for equivalence here.
    _private->scrollOrigin = scrollOrigin;
    id docView = [self documentView];

    NSRect visibleRect = [self documentVisibleRect];

    [docView setBoundsOrigin:NSMakePoint(-scrollOrigin.x, -scrollOrigin.y)];

    if (updatePositionAtAll)
        _private->scrollOriginChanged = true;

    // Maintain our original position in the presence of the new scroll origin.
    _private->scrollPositionExcludingOrigin = NSMakePoint(visibleRect.origin.x + scrollOrigin.x, visibleRect.origin.y + scrollOrigin.y);

    if (updatePositionAtAll && updatePositionSynchronously) // Otherwise we'll just let the snap happen when we update for the resize.
        [self adjustForScrollOriginChange];
}

- (NSPoint)scrollOrigin
{
    return _private->scrollOrigin;
}

- (BOOL)inProgrammaticScroll
{
    return _private->inProgrammaticScroll;
}

- (void)setContentInsets:(NSEdgeInsets)edgeInsets
{
    [super setContentInsets:edgeInsets];
    [self tile];
}

@end