WKPDFHUDView.mm   [plain text]


/*
 * Copyright (C) 2020 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 "config.h"
#import "WKPDFHUDView.h"

#if ENABLE(UI_PROCESS_PDF_HUD)

#import "WKWebViewInternal.h"
#import "WebPageProxy.h"
#import <QuartzCore/CATransaction.h>
#import <pal/spi/cocoa/QuartzCoreSPI.h>
#import <pal/spi/mac/NSImageSPI.h>
#import <wtf/WeakObjCPtr.h>

//  The HUD items should have the following spacing:
//  -------------------------------------------------
// |      12        12      10     12        12      |
// |     ----      ----     |     ----      ----     |
// | 10 |icon| 10 |icon| 10 | 10 |icon| 10 |icon| 10 |
// |     ----      ----     |     ----      ----     |
// |      12        12      10     12        12      |
//  -------------------------------------------------
//  where the 12 point vertical spacing is anchored to the smallest icon image,
//  and all subsequent icons with be centered vertically with the smallest icon.

static const CGFloat layerVerticalOffset = 40;
static const CGFloat layerCornerRadius = 12;
static const CGFloat layerGrayComponent = 0;
static const CGFloat layerAlpha = 0.75;
static const CGFloat layerImageScale = 1.5;
static const CGFloat layerSeperatorControllerSize = 1.5;
static const CGFloat layerControllerHorizontalMargin = 10.0;
static const CGFloat layerImageVerticalMargin = 12.0;
static const CGFloat layerSeperatorVerticalMargin = 10.0;
static const CGFloat controlLayerNormalAlpha = 0.75;
static const CGFloat controlLayerDownAlpha = 0.45;

static NSString * const PDFHUDZoomInControl = @"plus.magnifyingglass";
static NSString * const PDFHUDZoomOutControl = @"minus.magnifyingglass";
static NSString * const PDFHUDLaunchPreviewControl = @"preview";
static NSString * const PDFHUDSavePDFControl = @"arrow.down.circle";
static NSString * const PDFHUDSeperatorControl = @"PDFHUDSeperatorControl";

static const CGFloat layerFadeInTimeInterval = 0.25;
static const CGFloat layerFadeOutTimeInterval = 0.5;
static const CGFloat initialHideTimeInterval = 3.0;

static NSArray<NSString *> *controlArray()
{
    return @[
        PDFHUDZoomOutControl,
        PDFHUDZoomInControl,
        PDFHUDSeperatorControl,
        PDFHUDLaunchPreviewControl,
        PDFHUDSavePDFControl
    ];
}

@implementation WKPDFHUDView {
@private
    WeakPtr<WebKit::WebPageProxy> _page;
    RetainPtr<NSString> _activeControl;
    WebKit::PDFPluginIdentifier _pluginIdentifier;
    CGFloat _deviceScaleFactor;
    RetainPtr<CALayer> _layer;
    RetainPtr<CALayer> _activeLayer;
    CGSize _frameSize;
    RetainPtr<NSMutableDictionary<NSString *, NSImage *>> _cachedIcons;
    BOOL _visible;
    BOOL _mouseMovedToHUD;
    BOOL _initialHideTimerFired;
}

- (instancetype)initWithFrame:(NSRect)frame pluginIdentifier:(WebKit::PDFPluginIdentifier)pluginIdentifier page:(WebKit::WebPageProxy&)page
{
    if (!(self = [super initWithFrame:frame]))
        return nil;
    
    self.wantsLayer = YES;
    _cachedIcons = adoptNS([[NSMutableDictionary alloc] init]);
    _pluginIdentifier = pluginIdentifier;
    _page = makeWeakPtr(page);
    _deviceScaleFactor = page.deviceScaleFactor();
    _visible = YES;
    [self _setupLayer:self.layer];
    [self setFrame:frame];

    WeakObjCPtr<WKPDFHUDView> weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(initialHideTimeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf _hideTimerFired];
    });
    return self;
}

- (void)dealloc
{
    [_layer removeFromSuperlayer];
    [super dealloc];
}

- (void)setFrame:(NSRect)rect
{
    [super setFrame:rect];
    _frameSize = rect.size;

    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    CGRect layerBounds = [_layer bounds];
    [_layer setFrame:CGRectMake(rect.size.width / 2.0 - layerBounds.size.width / 2.0, layerVerticalOffset, layerBounds.size.width, layerBounds.size.height)];
    [CATransaction commit];
}

- (void)setDeviceScaleFactor:(CGFloat)deviceScaleFactor
{
    if (_deviceScaleFactor == deviceScaleFactor)
        return;
    
    _deviceScaleFactor = deviceScaleFactor;
    
    [self _redrawLayer];
}

- (void)_hideTimerFired
{
    _initialHideTimerFired = YES;
    if (!_mouseMovedToHUD)
        [self _setVisible:false];
}

- (void)_setVisible:(bool)isVisible
{
    if (_visible == isVisible)
        return;
    _visible = isVisible;
    [CATransaction begin];
    [CATransaction setAnimationDuration:isVisible ? layerFadeInTimeInterval : layerFadeOutTimeInterval];
    [self _setLayerOpacity:isVisible ? layerAlpha : 0.0];
    [CATransaction commit];
}

- (NSView *)hitTest:(NSPoint)point
{
    if (_page)
        return fromWebPageProxy(*_page);

    ASSERT_NOT_REACHED();
    return self;
}

- (void)mouseMoved:(NSEvent *)event
{
    if (CGRectContainsPoint([self convertRect:CGRectInset([_layer frame], -16.0, -16.0) toView:nil], NSPointToCGPoint(event.locationInWindow))) {
        [self _setVisible:true];
        _mouseMovedToHUD = YES;
    } else if (_initialHideTimerFired)
        [self _setVisible:false];
}

- (void)mouseDown:(NSEvent *)event
{
    _activeControl = [self _controlForEvent:event];
    if ([_activeControl isEqualToString:PDFHUDSeperatorControl])
        _activeControl = nil;
    if (_activeControl) {
        // Update rendering to highlight it..
        _activeLayer = [self _layerForEvent:event];
        
        // Update layer image; do not animate
        [CATransaction begin];
        [CATransaction setDisableActions:YES];
        
        [_activeLayer setOpacity:controlLayerDownAlpha];
        
        [CATransaction commit];
    }
}

- (void)mouseUp:(NSEvent *)event
{
    if (!_activeControl)
        return;
    
    NSString* mouseUpControl = [self _controlForEvent:event];
    if ([_activeControl isEqualToString:mouseUpControl])
        [self _performActionForControl:_activeControl.get()];
    
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    [_activeLayer setOpacity:controlLayerNormalAlpha];
    [CATransaction commit];

    _activeLayer = nil;
    _activeControl = nil;
}

- (Optional<NSUInteger>)_controlIndexForEvent:(NSEvent *)event
{
    CGPoint initialPoint = NSPointToCGPoint(event.locationInWindow);
    initialPoint.x -= [_layer frame].origin.x;
    initialPoint.y -= [_layer frame].origin.y;
    for (NSUInteger index = 0; index < [_layer sublayers].count; index++) {
        CALayer *subLayer = [_layer sublayers][index];
        NSRect windowSpaceRect = [self convertRect:subLayer.frame toView:nil];
        if (CGRectContainsPoint(windowSpaceRect, initialPoint))
            return index;
    }
    return WTF::nullopt;
}

- (NSString *)_controlForEvent:(NSEvent *)event
{
    if (auto index = [self _controlIndexForEvent:event])
        return controlArray()[*index];
    return nil;
}

- (CALayer *)_layerForEvent:(NSEvent *)event
{
    if (auto index = [self _controlIndexForEvent:event])
        return [_layer sublayers][*index];
    return nil;
}

- (void)_performActionForControl:(NSString *)control
{
    if (!_visible)
        return;
    auto* page = _page.get();
    if (!page)
        return;
    if ([control isEqualToString:PDFHUDZoomInControl])
        page->pdfZoomIn(_pluginIdentifier);
    else if ([control isEqualToString:PDFHUDZoomOutControl])
        page->pdfZoomOut(_pluginIdentifier);
    else if ([control isEqualToString:PDFHUDSavePDFControl])
        page->pdfSaveToPDF(_pluginIdentifier);
    else if ([control isEqualToString:PDFHUDLaunchPreviewControl])
        page->pdfOpenWithPreview(_pluginIdentifier);
}

- (void)_loadIconImages
{
    for (NSString *controlName in controlArray())
        [self _getImageForControlName:controlName];
}

- (void)_setupLayer:(CALayer *)parentLayer
{
    _layer = adoptNS([[CALayer alloc] init]);
    [_layer setCornerRadius:layerCornerRadius];
    
    [_layer setBackgroundColor:WebCore::cachedCGColor({ WebCore::SRGBA<float>(layerGrayComponent, layerGrayComponent, layerGrayComponent) })];
    [self _setLayerOpacity:layerAlpha];
    
    [self _loadIconImages];
    CGFloat minIconImageHeight = std::numeric_limits<CGFloat>::max();
    for (NSImage *image in [_cachedIcons allValues])
        minIconImageHeight = std::min(minIconImageHeight, (image.size.height / _deviceScaleFactor));
    
    CGFloat dx = layerControllerHorizontalMargin;
    for (NSString *controlName in controlArray()) {
        auto controlLayer = adoptNS([[CALayer alloc] init]);
        CGFloat dy = 0.0;
        CGFloat controllerWidth = 0.0;
        CGFloat controllerHeight = 0.0;

        if ([controlName isEqualToString:PDFHUDSeperatorControl]) {
            dy = layerSeperatorVerticalMargin;
            controllerWidth = layerSeperatorControllerSize;
            controllerHeight = minIconImageHeight + (2.0 * layerImageVerticalMargin) - (2.0 * layerSeperatorVerticalMargin);
            
            [controlLayer setBackgroundColor:[[NSColor lightGrayColor] CGColor]];
        } else {
            NSImage *controlImage = [self _getImageForControlName:controlName];
            [controlLayer setContents:controlImage];
            [controlLayer setOpacity:controlLayerNormalAlpha];

            dy = layerImageVerticalMargin;
            controllerWidth = [controlImage size].width / _deviceScaleFactor;
            controllerHeight = [controlImage size].height / _deviceScaleFactor;
            
            dy -= (controllerHeight - minIconImageHeight) / 2.0;
            
            [controlLayer setFilters:@[[CAFilter filterWithType:kCAFilterColorInvert]]];
        }
        
        [controlLayer setFrame:CGRectMake(dx, dy, controllerWidth, controllerHeight)];
        [_layer addSublayer:controlLayer.get()];
        
        dx += controllerWidth + layerControllerHorizontalMargin;
    }
    
    [_layer setFrame:CGRectMake(0, layerVerticalOffset, dx, minIconImageHeight + 2.0 * layerImageVerticalMargin)];
    [parentLayer addSublayer:_layer.get()];
}

- (void)_redrawLayer
{
    [_cachedIcons removeAllObjects];
    CALayer *parentLayer = [_layer superlayer];
    [_layer removeFromSuperlayer];
    [self _setupLayer:parentLayer];
    [self setFrameSize:_frameSize];
}

- (NSImage *)_getImageForControlName:(NSString *)control
{
    NSImage *iconImage = _cachedIcons.get()[control];
    if (iconImage)
        return iconImage;

    ALLOW_DEPRECATED_DECLARATIONS_BEGIN
    iconImage = [NSImage _imageWithSystemSymbolName:control];
    ALLOW_DEPRECATED_DECLARATIONS_END
    if (!iconImage)
        return nil;

    iconImage = [iconImage imageWithSymbolConfiguration:[NSImageSymbolConfiguration configurationWithTextStyle:NSFontTextStyleTitle2 scale:NSImageSymbolScaleLarge]];
        
    NSRect iconImageRect = NSMakeRect(0, 0, [iconImage size].width * layerImageScale * _deviceScaleFactor, [iconImage size].height * layerImageScale * _deviceScaleFactor);
    NSImageRep *iconImageRep = [iconImage bestRepresentationForRect:iconImageRect context:nil hints:nil];
    iconImage = [NSImage imageWithImageRep:iconImageRep];
    
    _cachedIcons.get()[control] = iconImage;
    return iconImage;
}

- (void)_setLayerOpacity:(CGFloat)alpha
{
    [_layer setOpacity:alpha];
    for (CALayer *subLayer in [_layer sublayers])
        [subLayer setOpacity:alpha];
}

@end

#endif // ENABLE(UI_PROCESS_PDF_HUD)