WebDynamicScrollBarsView.mm revision ab9e7a118cf1ea2e3a93dce683b2ded3e7291ddb
1/*
2 * Copyright (C) 2005, 2008, 2010 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
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.
12 *
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.
24 */
25
26#import "WebDynamicScrollBarsViewInternal.h"
27
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>
35
36using namespace WebCore;
37
38// FIXME: <rdar://problem/5898985> Mail expects a constant of this name to exist.
39const int WebCoreScrollbarAlwaysOn = ScrollbarAlwaysOn;
40
41#ifndef __OBJC2__
42// In <rdar://problem/7814899> we saw crashes because WebDynamicScrollBarsView increased in size, breaking ABI compatiblity.
43COMPILE_ASSERT(sizeof(WebDynamicScrollBarsView) == 0x8c, WebDynamicScrollBarsView_is_expected_size);
44#endif
45
46struct WebDynamicScrollBarsViewPrivate {
47    unsigned inUpdateScrollersLayoutPass;
48
49    WebCore::ScrollbarMode hScroll;
50    WebCore::ScrollbarMode vScroll;
51
52    bool hScrollModeLocked;
53    bool vScrollModeLocked;
54    bool suppressLayout;
55    bool suppressScrollers;
56    bool inUpdateScrollers;
57    bool verticallyPinnedByPreviousWheelEvent;
58    bool horizontallyPinnedByPreviousWheelEvent;
59
60    bool allowsScrollersToOverlapContent;
61    bool alwaysHideHorizontalScroller;
62    bool alwaysHideVerticalScroller;
63    bool horizontalScrollingAllowedButScrollerHidden;
64    bool verticalScrollingAllowedButScrollerHidden;
65
66    // scrollOrigin is set for various combinations of writing mode and direction.
67    // See the comment next to the corresponding member in ScrollView.h.
68    NSPoint scrollOrigin;
69
70    // Flag to indicate that the scrollbar thumb's initial position needs to
71    // be manually set.
72    bool scrollOriginChanged;
73    NSPoint scrollPositionExcludingOrigin;
74
75    bool inProgrammaticScroll;
76};
77
78@implementation WebDynamicScrollBarsView
79
80- (id)initWithFrame:(NSRect)frame
81{
82    if (!(self = [super initWithFrame:frame]))
83        return nil;
84
85    _private = new WebDynamicScrollBarsViewPrivate;
86    memset(_private, 0, sizeof(WebDynamicScrollBarsViewPrivate));
87    return self;
88}
89
90- (id)initWithCoder:(NSCoder *)aDecoder
91{
92    if (!(self = [super initWithCoder:aDecoder]))
93        return nil;
94
95    _private = new WebDynamicScrollBarsViewPrivate;
96    memset(_private, 0, sizeof(WebDynamicScrollBarsViewPrivate));
97    return self;
98}
99
100- (void)dealloc
101{
102    delete _private;
103    [super dealloc];
104}
105
106- (void)finalize
107{
108    delete _private;
109    [super finalize];
110}
111
112- (void)setAllowsHorizontalScrolling:(BOOL)flag
113{
114    if (_private->hScrollModeLocked)
115        return;
116    if (flag && _private->hScroll == ScrollbarAlwaysOff)
117        _private->hScroll = ScrollbarAuto;
118    else if (!flag && _private->hScroll != ScrollbarAlwaysOff)
119        _private->hScroll = ScrollbarAlwaysOff;
120    [self updateScrollers];
121}
122
123- (void)setAllowsScrollersToOverlapContent:(BOOL)flag
124{
125    if (_private->allowsScrollersToOverlapContent == flag)
126        return;
127
128    _private->allowsScrollersToOverlapContent = flag;
129
130    [[self contentView] setFrame:[self contentViewFrame]];
131    [[self documentView] setNeedsLayout:YES];
132    [[self documentView] layout];
133}
134
135- (void)setAlwaysHideHorizontalScroller:(BOOL)shouldBeHidden
136{
137    if (_private->alwaysHideHorizontalScroller == shouldBeHidden)
138        return;
139
140    _private->alwaysHideHorizontalScroller = shouldBeHidden;
141    [self updateScrollers];
142}
143
144- (void)setAlwaysHideVerticalScroller:(BOOL)shouldBeHidden
145{
146    if (_private->alwaysHideVerticalScroller == shouldBeHidden)
147        return;
148
149    _private->alwaysHideVerticalScroller = shouldBeHidden;
150    [self updateScrollers];
151}
152
153- (BOOL)horizontalScrollingAllowed
154{
155    return _private->horizontalScrollingAllowedButScrollerHidden || [self hasHorizontalScroller];
156}
157
158- (BOOL)verticalScrollingAllowed
159{
160    return _private->verticalScrollingAllowedButScrollerHidden || [self hasVerticalScroller];
161}
162
163- (BOOL)inProgramaticScroll
164{
165    return _private->inProgrammaticScroll;
166}
167
168@end
169
170@implementation WebDynamicScrollBarsView (WebInternal)
171
172- (NSRect)contentViewFrame
173{
174    NSRect frame = [[self contentView] frame];
175
176    if ([self hasHorizontalScroller])
177        frame.size.height = (_private->allowsScrollersToOverlapContent ? NSMaxY([[self horizontalScroller] frame]) : NSMinY([[self horizontalScroller] frame]));
178    if ([self hasVerticalScroller])
179        frame.size.width = (_private->allowsScrollersToOverlapContent ? NSMaxX([[self verticalScroller] frame]) : NSMinX([[self verticalScroller] frame]));
180    return frame;
181}
182
183- (void)tile
184{
185    [super tile];
186
187    // [super tile] sets the contentView size so that it does not overlap with the scrollers,
188    // we want to re-set the contentView to overlap scrollers before displaying.
189    if (_private->allowsScrollersToOverlapContent)
190        [[self contentView] setFrame:[self contentViewFrame]];
191}
192
193- (void)setSuppressLayout:(BOOL)flag
194{
195    _private->suppressLayout = flag;
196}
197
198- (void)setScrollBarsSuppressed:(BOOL)suppressed repaintOnUnsuppress:(BOOL)repaint
199{
200    _private->suppressScrollers = suppressed;
201
202    // This code was originally changes for a Leopard performance imporvement. We decided to
203    // ifdef it to fix correctness issues on Tiger documented in <rdar://problem/5441823>.
204#ifndef BUILDING_ON_TIGER
205    if (suppressed) {
206        [[self verticalScroller] setNeedsDisplay:NO];
207        [[self horizontalScroller] setNeedsDisplay:NO];
208    }
209
210    if (!suppressed && repaint)
211        [super reflectScrolledClipView:[self contentView]];
212#else
213    if (suppressed || repaint) {
214        [[self verticalScroller] setNeedsDisplay:!suppressed];
215        [[self horizontalScroller] setNeedsDisplay:!suppressed];
216    }
217#endif
218}
219
220- (void)adjustForScrollOriginChange
221{
222    if (!_private->scrollOriginChanged)
223        return;
224
225    _private->scrollOriginChanged = false;
226
227    NSView *documentView = [self documentView];
228    NSRect documentRect = [documentView bounds];
229
230    // The call to [NSView scrollPoint:] fires off notification the handler for which needs to know that
231    // we're setting the initial scroll position so it doesn't interpret this as a user action and
232    // fire off a JS event.
233    _private->inProgrammaticScroll = true;
234    [documentView scrollPoint:NSMakePoint(_private->scrollPositionExcludingOrigin.x + documentRect.origin.x, _private->scrollPositionExcludingOrigin.y + documentRect.origin.y)];
235    _private->inProgrammaticScroll = false;
236}
237
238static const unsigned cMaxUpdateScrollbarsPass = 2;
239
240- (void)updateScrollers
241{
242    NSView *documentView = [self documentView];
243
244    // If we came in here with the view already needing a layout, then go ahead and do that
245    // first.  (This will be the common case, e.g., when the page changes due to window resizing for example).
246    // This layout will not re-enter updateScrollers and does not count towards our max layout pass total.
247    if (!_private->suppressLayout && !_private->suppressScrollers && [documentView isKindOfClass:[WebHTMLView class]]) {
248        WebHTMLView* htmlView = (WebHTMLView*)documentView;
249        if ([htmlView _needsLayout]) {
250            _private->inUpdateScrollers = YES;
251            [(id <WebDocumentView>)documentView layout];
252            _private->inUpdateScrollers = NO;
253        }
254    }
255
256    BOOL hasHorizontalScroller = [self hasHorizontalScroller];
257    BOOL hasVerticalScroller = [self hasVerticalScroller];
258
259    BOOL newHasHorizontalScroller = hasHorizontalScroller;
260    BOOL newHasVerticalScroller = hasVerticalScroller;
261
262    if (!documentView) {
263        newHasHorizontalScroller = NO;
264        newHasVerticalScroller = NO;
265    }
266
267    if (_private->hScroll != ScrollbarAuto)
268        newHasHorizontalScroller = (_private->hScroll == ScrollbarAlwaysOn);
269    if (_private->vScroll != ScrollbarAuto)
270        newHasVerticalScroller = (_private->vScroll == ScrollbarAlwaysOn);
271
272    if (!documentView || _private->suppressLayout || _private->suppressScrollers || (_private->hScroll != ScrollbarAuto && _private->vScroll != ScrollbarAuto)) {
273        _private->horizontalScrollingAllowedButScrollerHidden = newHasHorizontalScroller && _private->alwaysHideHorizontalScroller;
274        if (_private->horizontalScrollingAllowedButScrollerHidden)
275            newHasHorizontalScroller = NO;
276
277        _private->verticalScrollingAllowedButScrollerHidden = newHasVerticalScroller && _private->alwaysHideVerticalScroller;
278        if (_private->verticalScrollingAllowedButScrollerHidden)
279            newHasVerticalScroller = NO;
280
281        _private->inUpdateScrollers = YES;
282        if (hasHorizontalScroller != newHasHorizontalScroller)
283            [self setHasHorizontalScroller:newHasHorizontalScroller];
284        if (hasVerticalScroller != newHasVerticalScroller)
285            [self setHasVerticalScroller:newHasVerticalScroller];
286        if (_private->suppressScrollers) {
287            [[self verticalScroller] setNeedsDisplay:NO];
288            [[self horizontalScroller] setNeedsDisplay:NO];
289        }
290        _private->inUpdateScrollers = NO;
291        return;
292    }
293
294    BOOL needsLayout = NO;
295
296    NSSize documentSize = [documentView frame].size;
297    NSSize visibleSize = [self documentVisibleRect].size;
298    NSSize frameSize = [self frame].size;
299
300    // When in HiDPI with a scale factor > 1, the visibleSize and frameSize may be non-integral values,
301    // while the documentSize (set by WebCore) will be integral.  Round up the non-integral sizes so that
302    // the mismatch won't cause unwanted scrollbars to appear.  This can result in slightly cut off content,
303    // but it will always be less than one pixel, which should not be noticeable.
304    visibleSize.width = ceilf(visibleSize.width);
305    visibleSize.height = ceilf(visibleSize.height);
306    frameSize.width = ceilf(frameSize.width);
307    frameSize.height = ceilf(frameSize.height);
308
309    if (_private->hScroll == ScrollbarAuto) {
310        newHasHorizontalScroller = documentSize.width > visibleSize.width;
311        if (newHasHorizontalScroller && !_private->inUpdateScrollersLayoutPass && documentSize.height <= frameSize.height && documentSize.width <= frameSize.width)
312            newHasHorizontalScroller = NO;
313    }
314
315    if (_private->vScroll == ScrollbarAuto) {
316        newHasVerticalScroller = documentSize.height > visibleSize.height;
317        if (newHasVerticalScroller && !_private->inUpdateScrollersLayoutPass && documentSize.height <= frameSize.height && documentSize.width <= frameSize.width)
318            newHasVerticalScroller = NO;
319    }
320
321    // Unless in ScrollbarsAlwaysOn mode, if we ever turn one scrollbar off, always turn the other one off too.
322    // Never ever try to both gain/lose a scrollbar in the same pass.
323    if (!newHasHorizontalScroller && hasHorizontalScroller && _private->vScroll != ScrollbarAlwaysOn)
324        newHasVerticalScroller = NO;
325    if (!newHasVerticalScroller && hasVerticalScroller && _private->hScroll != ScrollbarAlwaysOn)
326        newHasHorizontalScroller = NO;
327
328    _private->horizontalScrollingAllowedButScrollerHidden = newHasHorizontalScroller && _private->alwaysHideHorizontalScroller;
329    if (_private->horizontalScrollingAllowedButScrollerHidden)
330        newHasHorizontalScroller = NO;
331
332    _private->verticalScrollingAllowedButScrollerHidden = newHasVerticalScroller && _private->alwaysHideVerticalScroller;
333    if (_private->verticalScrollingAllowedButScrollerHidden)
334        newHasVerticalScroller = NO;
335
336    if (hasHorizontalScroller != newHasHorizontalScroller) {
337        _private->inUpdateScrollers = YES;
338        [self setHasHorizontalScroller:newHasHorizontalScroller];
339        _private->inUpdateScrollers = NO;
340        needsLayout = YES;
341        NSView *documentView = [self documentView];
342        NSRect documentRect = [documentView bounds];
343        if (documentRect.origin.y < 0 && !newHasHorizontalScroller)
344            [documentView setBoundsOrigin:NSMakePoint(documentRect.origin.x, documentRect.origin.y + 15)];
345    }
346
347    if (hasVerticalScroller != newHasVerticalScroller) {
348        _private->inUpdateScrollers = YES;
349        [self setHasVerticalScroller:newHasVerticalScroller];
350        _private->inUpdateScrollers = NO;
351        needsLayout = YES;
352        NSView *documentView = [self documentView];
353        NSRect documentRect = [documentView bounds];
354        if (documentRect.origin.x < 0 && !newHasVerticalScroller)
355            [documentView setBoundsOrigin:NSMakePoint(documentRect.origin.x + 15, documentRect.origin.y)];
356    }
357
358    if (needsLayout && _private->inUpdateScrollersLayoutPass < cMaxUpdateScrollbarsPass &&
359        [documentView conformsToProtocol:@protocol(WebDocumentView)]) {
360        _private->inUpdateScrollersLayoutPass++;
361        [(id <WebDocumentView>)documentView setNeedsLayout:YES];
362        [(id <WebDocumentView>)documentView layout];
363        NSSize newDocumentSize = [documentView frame].size;
364        if (NSEqualSizes(documentSize, newDocumentSize)) {
365            // The layout with the new scroll state had no impact on
366            // the document's overall size, so updateScrollers didn't get called.
367            // Recur manually.
368            [self updateScrollers];
369        }
370        _private->inUpdateScrollersLayoutPass--;
371    }
372}
373
374// Make the horizontal and vertical scroll bars come and go as needed.
375- (void)reflectScrolledClipView:(NSClipView *)clipView
376{
377    if (clipView == [self contentView]) {
378        // Prevent appearance of trails because of overlapping views
379        if (_private->allowsScrollersToOverlapContent)
380            [self setDrawsBackground:NO];
381
382        // FIXME: This hack here prevents infinite recursion that takes place when we
383        // gyrate between having a vertical scroller and not having one. A reproducible
384        // case is clicking on the "the Policy Routing text" link at
385        // http://www.linuxpowered.com/archive/howto/Net-HOWTO-8.html.
386        // The underlying cause is some problem in the NSText machinery, but I was not
387        // able to pin it down.
388        NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
389        if (!_private->inUpdateScrollers && (!currentContext || [currentContext isDrawingToScreen]))
390            [self updateScrollers];
391    }
392
393    // This code was originally changed for a Leopard performance imporvement. We decided to
394    // ifdef it to fix correctness issues on Tiger documented in <rdar://problem/5441823>.
395#ifndef BUILDING_ON_TIGER
396    // Update the scrollers if they're not being suppressed.
397    if (!_private->suppressScrollers)
398        [super reflectScrolledClipView:clipView];
399#else
400    [super reflectScrolledClipView:clipView];
401
402    // Validate the scrollers if they're being suppressed.
403    if (_private->suppressScrollers) {
404        [[self verticalScroller] setNeedsDisplay:NO];
405        [[self horizontalScroller] setNeedsDisplay:NO];
406    }
407#endif
408
409    // The call to [NSView reflectScrolledClipView] sets the scrollbar thumb
410    // position to 0 (the left) when the view is initially displayed.
411    // This call updates the initial position correctly.
412    [self adjustForScrollOriginChange];
413
414#if USE(ACCELERATED_COMPOSITING) && defined(BUILDING_ON_LEOPARD)
415    NSView *documentView = [self documentView];
416    if ([documentView isKindOfClass:[WebHTMLView class]]) {
417        WebHTMLView *htmlView = (WebHTMLView *)documentView;
418        if ([htmlView _isUsingAcceleratedCompositing])
419            [htmlView _updateLayerHostingViewPosition];
420    }
421#endif
422}
423
424- (BOOL)allowsHorizontalScrolling
425{
426    return _private->hScroll != ScrollbarAlwaysOff;
427}
428
429- (BOOL)allowsVerticalScrolling
430{
431    return _private->vScroll != ScrollbarAlwaysOff;
432}
433
434- (void)scrollingModes:(WebCore::ScrollbarMode*)hMode vertical:(WebCore::ScrollbarMode*)vMode
435{
436    *hMode = _private->hScroll;
437    *vMode = _private->vScroll;
438}
439
440- (ScrollbarMode)horizontalScrollingMode
441{
442    return _private->hScroll;
443}
444
445- (ScrollbarMode)verticalScrollingMode
446{
447    return _private->vScroll;
448}
449
450- (void)setHorizontalScrollingMode:(ScrollbarMode)horizontalMode andLock:(BOOL)lock
451{
452    [self setScrollingModes:horizontalMode vertical:[self verticalScrollingMode] andLock:lock];
453}
454
455- (void)setVerticalScrollingMode:(ScrollbarMode)verticalMode andLock:(BOOL)lock
456{
457    [self setScrollingModes:[self horizontalScrollingMode] vertical:verticalMode andLock:lock];
458}
459
460// Mail uses this method, so we cannot remove it.
461- (void)setVerticalScrollingMode:(ScrollbarMode)verticalMode
462{
463    [self setScrollingModes:[self horizontalScrollingMode] vertical:verticalMode andLock:NO];
464}
465
466- (void)setScrollingModes:(ScrollbarMode)horizontalMode vertical:(ScrollbarMode)verticalMode andLock:(BOOL)lock
467{
468    BOOL update = NO;
469    if (verticalMode != _private->vScroll && !_private->vScrollModeLocked) {
470        _private->vScroll = verticalMode;
471        update = YES;
472    }
473
474    if (horizontalMode != _private->hScroll && !_private->hScrollModeLocked) {
475        _private->hScroll = horizontalMode;
476        update = YES;
477    }
478
479    if (lock)
480        [self setScrollingModesLocked:YES];
481
482    if (update)
483        [self updateScrollers];
484}
485
486- (void)setHorizontalScrollingModeLocked:(BOOL)locked
487{
488    _private->hScrollModeLocked = locked;
489}
490
491- (void)setVerticalScrollingModeLocked:(BOOL)locked
492{
493    _private->vScrollModeLocked = locked;
494}
495
496- (void)setScrollingModesLocked:(BOOL)locked
497{
498    _private->hScrollModeLocked = _private->vScrollModeLocked = locked;
499}
500
501- (BOOL)horizontalScrollingModeLocked
502{
503    return _private->hScrollModeLocked;
504}
505
506- (BOOL)verticalScrollingModeLocked
507{
508    return _private->vScrollModeLocked;
509}
510
511- (BOOL)autoforwardsScrollWheelEvents
512{
513    return YES;
514}
515
516- (void)scrollWheel:(NSEvent *)event
517{
518    float deltaX;
519    float deltaY;
520    BOOL isContinuous;
521    WKGetWheelEventDeltas(event, &deltaX, &deltaY, &isContinuous);
522
523    BOOL isLatchingEvent = WKIsLatchingWheelEvent(event);
524
525    if (fabsf(deltaY) > fabsf(deltaX)) {
526        if (![self allowsVerticalScrolling]) {
527            [[self nextResponder] scrollWheel:event];
528            return;
529        }
530
531        if (isLatchingEvent && !_private->verticallyPinnedByPreviousWheelEvent) {
532            double verticalPosition = [[self verticalScroller] doubleValue];
533            if ((deltaY >= 0.0 && verticalPosition == 0.0) || (deltaY <= 0.0 && verticalPosition == 1.0))
534                return;
535        }
536    } else {
537        if (![self allowsHorizontalScrolling]) {
538            [[self nextResponder] scrollWheel:event];
539            return;
540        }
541
542        if (isLatchingEvent && !_private->horizontallyPinnedByPreviousWheelEvent) {
543            double horizontalPosition = [[self horizontalScroller] doubleValue];
544            if ((deltaX >= 0.0 && horizontalPosition == 0.0) || (deltaX <= 0.0 && horizontalPosition == 1.0))
545                return;
546        }
547    }
548
549    // Calling super can release the last reference. <rdar://problem/7400263>
550    // Hold a reference so the code following the super call will not crash.
551    [self retain];
552
553    [super scrollWheel:event];
554
555    if (!isLatchingEvent) {
556        double verticalPosition = [[self verticalScroller] doubleValue];
557        double horizontalPosition = [[self horizontalScroller] doubleValue];
558
559        _private->verticallyPinnedByPreviousWheelEvent = (verticalPosition == 0.0 || verticalPosition == 1.0);
560        _private->horizontallyPinnedByPreviousWheelEvent = (horizontalPosition == 0.0 || horizontalPosition == 1.0);
561    }
562
563    [self release];
564}
565
566- (BOOL)accessibilityIsIgnored
567{
568    return YES;
569}
570
571- (void)setScrollOrigin:(NSPoint)scrollOrigin updatePosition:(BOOL)updatePosition
572{
573    // The cross-platform ScrollView call already checked to see if the old/new scroll origins were the same or not
574    // so we don't have to check for equivalence here.
575    _private->scrollOrigin = scrollOrigin;
576    id docView = [self documentView];
577
578    NSRect visibleRect = [self documentVisibleRect];
579
580    [docView setBoundsOrigin:NSMakePoint(-scrollOrigin.x, -scrollOrigin.y)];
581
582    _private->scrollOriginChanged = true;
583
584    // Maintain our original position in the presence of the new scroll origin.
585    _private->scrollPositionExcludingOrigin = NSMakePoint(visibleRect.origin.x + scrollOrigin.x, visibleRect.origin.y + scrollOrigin.y);
586
587    if (updatePosition) // Otherwise we'll just let the snap happen when we update for the resize.
588        [self adjustForScrollOriginChange];
589}
590
591- (NSPoint)scrollOrigin
592{
593    return _private->scrollOrigin;
594}
595
596@end
597