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