/* * 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 #import #import using namespace WebCore; // FIXME: Mail expects a constant of this name to exist. const int WebCoreScrollbarAlwaysOn = ScrollbarAlwaysOn; #ifndef __OBJC2__ // In 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; // 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 - (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 . #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 } - (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 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 )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 )documentView setNeedsLayout:YES]; [(id )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 . #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 // 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]; #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); #if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD) NSEventPhase momentumPhase = [event momentumPhase]; BOOL isLatchingEvent = momentumPhase & NSEventPhaseBegan || momentumPhase & NSEventPhaseChanged; #else int momentumPhase = WKGetNSEventMomentumPhase(event); BOOL isLatchingEvent = momentumPhase == WKEventPhaseBegan || momentumPhase == WKEventPhaseChanged; #endif 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. // 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; } @end