/*
* Copyright (C) 2005, 2006 Apple Computer, 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.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "WebPDFView.h"
#import "WebDataSourceInternal.h"
#import "WebDocumentInternal.h"
#import "WebDocumentPrivate.h"
#import "WebFrame.h"
#import "WebFrameInternal.h"
#import "WebFrameView.h"
#import "WebLocalizableStrings.h"
#import "WebNSAttributedStringExtras.h"
#import "WebNSPasteboardExtras.h"
#import "WebNSViewExtras.h"
#import "WebPDFRepresentation.h"
#import "WebPreferencesPrivate.h"
#import "WebUIDelegate.h"
#import "WebView.h"
#import "WebViewInternal.h"
#import <WebCore/EventNames.h>
#import <WebCore/KeyboardEvent.h>
#import <WebCore/MouseEvent.h>
#import <WebCore/PlatformKeyboardEvent.h>
#import <JavaScriptCore/Assertions.h>
#import <PDFKit/PDFKit.h>
#import <WebCore/FrameLoader.h>
#import <WebCore/KURL.h>
#import <WebKitSystemInterface.h>
using namespace WebCore;
using namespace EventNames;
#define PDFKitLaunchNotification @"PDFPreviewLaunchPreview"
// QuartzPrivate.h doesn't include the PDFKit private headers, so we can't get at PDFViewPriv.h. (3957971)
// Even if that was fixed, we'd have to tweak compile options to include QuartzPrivate.h. (3957839)
@interface WebPDFView (FileInternal)
+ (Class)_PDFPreviewViewClass;
+ (Class)_PDFViewClass;
- (BOOL)_anyPDFTagsFoundInMenu:(NSMenu *)menu;
- (void)_applyPDFDefaults;
- (NSEvent *)_fakeKeyEventWithFunctionKey:(unichar)functionKey;
- (NSMutableArray *)_menuItemsFromPDFKitForEvent:(NSEvent *)theEvent;
- (void)_openWithFinder:(id)sender;
- (NSString *)_path;
- (PDFView *)_PDFSubview;
- (BOOL)_pointIsInSelection:(NSPoint)point;
- (void)_receivedPDFKitLaunchNotification:(NSNotification *)notification;
- (NSAttributedString *)_scaledAttributedString:(NSAttributedString *)unscaledAttributedString;
- (NSString *)_temporaryPDFDirectoryPath;
- (void)_trackFirstResponder;
@end;
// PDFPrefUpdatingProxy is a class that forwards everything it gets to a target and updates the PDF viewing prefs
// after each of those messages. We use it as a way to hook all the places that the PDF viewing attrs change.
@interface PDFPrefUpdatingProxy : NSProxy {
WebPDFView *view;
}
- (id)initWithView:(WebPDFView *)view;
@end
@interface PDFDocument (PDFKitSecretsIKnow)
- (NSPrintOperation *)getPrintOperationForPrintInfo:(NSPrintInfo *)printInfo autoRotate:(BOOL)doRotate;
@end
extern "C" NSString *_NSPathForSystemFramework(NSString *framework);
#pragma mark C UTILITY FUNCTIONS
static void _applicationInfoForMIMEType(NSString *type, NSString **name, NSImage **image)
{
NSURL *appURL = nil;
OSStatus error = LSCopyApplicationForMIMEType((CFStringRef)type, kLSRolesAll, (CFURLRef *)&appURL);
if (error != noErr)
return;
NSString *appPath = [appURL path];
CFRelease (appURL);
*image = [[NSWorkspace sharedWorkspace] iconForFile:appPath];
[*image setSize:NSMakeSize(16.f,16.f)];
NSString *appName = [[NSFileManager defaultManager] displayNameAtPath:appPath];
*name = appName;
}
// FIXME 4182876: We can eliminate this function in favor if -isEqual: if [PDFSelection isEqual:] is overridden
// to compare contents.
static BOOL _PDFSelectionsAreEqual(PDFSelection *selectionA, PDFSelection *selectionB)
{
NSArray *aPages = [selectionA pages];
NSArray *bPages = [selectionB pages];
if (![aPages isEqual:bPages])
return NO;
int count = [aPages count];
int i;
for (i = 0; i < count; ++i) {
NSRect aBounds = [selectionA boundsForPage:[aPages objectAtIndex:i]];
NSRect bBounds = [selectionB boundsForPage:[bPages objectAtIndex:i]];
if (!NSEqualRects(aBounds, bBounds)) {
return NO;
}
}
return YES;
}
@implementation WebPDFView
#pragma mark WebPDFView API
+ (NSBundle *)PDFKitBundle
{
static NSBundle *PDFKitBundle = nil;
if (PDFKitBundle == nil) {
NSString *PDFKitPath = [_NSPathForSystemFramework(@"Quartz.framework") stringByAppendingString:@"/Frameworks/PDFKit.framework"];
if (PDFKitPath == nil) {
LOG_ERROR("Couldn't find PDFKit.framework");
return nil;
}
PDFKitBundle = [NSBundle bundleWithPath:PDFKitPath];
if (![PDFKitBundle load]) {
LOG_ERROR("Couldn't load PDFKit.framework");
}
}
return PDFKitBundle;
}
+ (NSArray *)supportedMIMETypes
{
return [WebPDFRepresentation supportedMIMETypes];
}
- (void)setPDFDocument:(PDFDocument *)doc
{
[PDFSubview setDocument:doc];
[self _applyPDFDefaults];
}
#pragma mark NSObject OVERRIDES
- (void)dealloc
{
ASSERT(!trackedFirstResponder);
[previewView release];
[PDFSubview release];
[path release];
[PDFSubviewProxy release];
[super dealloc];
}
#pragma mark NSResponder OVERRIDES
- (void)centerSelectionInVisibleArea:(id)sender
{
[PDFSubview scrollSelectionToVisible:nil];
}
- (void)scrollPageDown:(id)sender
{
// PDFView doesn't support this responder method directly, so we pass it a fake key event
[PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSPageDownFunctionKey]];
}
- (void)scrollPageUp:(id)sender
{
// PDFView doesn't support this responder method directly, so we pass it a fake key event
[PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSPageUpFunctionKey]];
}
- (void)scrollLineDown:(id)sender
{
// PDFView doesn't support this responder method directly, so we pass it a fake key event
[PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSDownArrowFunctionKey]];
}
- (void)scrollLineUp:(id)sender
{
// PDFView doesn't support this responder method directly, so we pass it a fake key event
[PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSUpArrowFunctionKey]];
}
- (void)scrollToBeginningOfDocument:(id)sender
{
// PDFView doesn't support this responder method directly, so we pass it a fake key event
[PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSHomeFunctionKey]];
}
- (void)scrollToEndOfDocument:(id)sender
{
// PDFView doesn't support this responder method directly, so we pass it a fake key event
[PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSEndFunctionKey]];
}
// jumpToSelection is the old name for what AppKit now calls centerSelectionInVisibleArea. Safari
// was using the old jumpToSelection selector in its menu. Newer versions of Safari will us the
// selector centerSelectionInVisibleArea. We'll leave this old selector in place for two reasons:
// (1) compatibility between older Safari and newer WebKit; (2) other WebKit-based applications
// might be using the jumpToSelection: selector, and we don't want to break them.
- (void)jumpToSelection:(id)sender
{
[self centerSelectionInVisibleArea:nil];
}
#pragma mark NSView OVERRIDES
- (BOOL)acceptsFirstResponder {
return YES;
}
- (BOOL)becomeFirstResponder
{
// This works together with setNextKeyView to splice our PDFSubview into
// the key loop similar to the way NSScrollView does this.
NSWindow *window = [self window];
id newFirstResponder = nil;
if ([window keyViewSelectionDirection] == NSSelectingPrevious) {
NSView *previousValidKeyView = [self previousValidKeyView];
if ((previousValidKeyView != self) && (previousValidKeyView != PDFSubview))
newFirstResponder = previousValidKeyView;
} else {
NSView *PDFDocumentView = [PDFSubview documentView];
if ([PDFDocumentView acceptsFirstResponder])
newFirstResponder = PDFDocumentView;
}
if (!newFirstResponder)
return NO;
if (![window makeFirstResponder:newFirstResponder])
return NO;
[[dataSource webFrame] _clearSelectionInOtherFrames];
return YES;
}
- (NSView *)hitTest:(NSPoint)point
{
// Override hitTest so we can override menuForEvent.
NSEvent *event = [NSApp currentEvent];
NSEventType type = [event type];
if (type == NSRightMouseDown || (type == NSLeftMouseDown && ([event modifierFlags] & NSControlKeyMask)))
return self;
return [super hitTest:point];
}
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
Class previewViewClass = nil;
if ([[WebPreferences standardPreferences] _usePDFPreviewView])
previewViewClass = [[self class] _PDFPreviewViewClass];
// We might not have found a previewViewClass even if we looked for one.
// But if we found the class we should be able to create an instance.
if (previewViewClass) {
previewView = [[previewViewClass alloc] initWithFrame:frame];
ASSERT(previewView);
}
NSView *topLevelPDFKitView = nil;
if (previewView) {
// We'll retain the PDFSubview here so that it is equally retained in all
// code paths. That way we don't need to worry about conditionally releasing
// it later.
PDFSubview = [[previewView performSelector:@selector(pdfView)] retain];
topLevelPDFKitView = previewView;
} else {
PDFSubview = [[[[self class] _PDFViewClass] alloc] initWithFrame:frame];
topLevelPDFKitView = PDFSubview;
}
ASSERT(PDFSubview);
[topLevelPDFKitView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
[self addSubview:topLevelPDFKitView];
[PDFSubview setDelegate:self];
written = NO;
// Messaging this proxy is the same as messaging PDFSubview, with the side effect that the
// PDF viewing defaults are updated afterwards
PDFSubviewProxy = (PDFView *)[[PDFPrefUpdatingProxy alloc] initWithView:self];
}
return self;
}
- (NSMenu *)menuForEvent:(NSEvent *)theEvent
{
// Start with the menu items supplied by PDFKit, with WebKit tags applied
NSMutableArray *items = [self _menuItemsFromPDFKitForEvent:theEvent];
// Add in an "Open with <default PDF viewer>" item
NSString *appName = nil;
NSImage *appIcon = nil;
_applicationInfoForMIMEType([[dataSource response] MIMEType], &appName, &appIcon);
if (!appName)
appName = UI_STRING("Finder", "Default application name for Open With context menu");
// To match the PDFKit style, we'll add Open with Preview even when there's no document yet to view, and
// disable it using validateUserInterfaceItem.
NSString *title = [NSString stringWithFormat:UI_STRING("Open with %@", "context menu item for PDF"), appName];
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:@selector(_openWithFinder:) keyEquivalent:@""];
[item setTag:WebMenuItemTagOpenWithDefaultApplication];
if (appIcon)
[item setImage:appIcon];
[items insertObject:item atIndex:0];
[item release];
[items insertObject:[NSMenuItem separatorItem] atIndex:1];
// pass the items off to the WebKit context menu mechanism
WebView *webView = [[dataSource webFrame] webView];
ASSERT(webView);
NSMenu *menu = [webView _menuForElement:[self elementAtPoint:[self convertPoint:[theEvent locationInWindow] fromView:nil]] defaultItems:items];
// The delegate has now had the opportunity to add items to the standard PDF-related items, or to
// remove or modify some of the PDF-related items. In 10.4, the PDF context menu did not go through
// the standard WebKit delegate path, and so the standard PDF-related items always appeared. For
// clients that create their own context menu by hand-picking specific items from the default list, such as
// Safari, none of the PDF-related items will appear until the client is rewritten to explicitly
// include these items. For backwards compatibility of tip-of-tree WebKit with the 10.4 version of Safari
// (the configuration that people building open source WebKit use), we'll use the entire set of PDFKit-supplied
// menu items. This backward-compatibility hack won't work with any non-Safari clients, but this seems OK since
// (1) the symptom is fairly minor, and (2) we suspect that non-Safari clients are probably using the entire
// set of default items, rather than manually choosing from them. We can remove this code entirely when we
// ship a version of Safari that includes the fix for radar 3796579.
if (![self _anyPDFTagsFoundInMenu:menu] && [[[NSBundle mainBundle] bundleIdentifier] isEqualToString:@"com.apple.Safari"]) {
[menu addItem:[NSMenuItem separatorItem]];
NSEnumerator *e = [items objectEnumerator];
NSMenuItem *menuItem;
while ((menuItem = [e nextObject]) != nil) {
// copy menuItem since a given menuItem can be in only one menu at a time, and we don't
// want to mess with the menu returned from PDFKit.
[menu addItem:[menuItem copy]];
}
}
return menu;
}
- (void)setNextKeyView:(NSView *)aView
{
// This works together with becomeFirstResponder to splice PDFSubview into
// the key loop similar to the way NSScrollView and NSClipView do this.
NSView *documentView = [PDFSubview documentView];
if (documentView) {
[documentView setNextKeyView:aView];
// We need to make the documentView be the next view in the keyview loop.
// It would seem more sensible to do this in our init method, but it turns out
// that [NSClipView setDocumentView] won't call this method if our next key view
// is already set, so we wait until we're called before adding this connection.
// We'll also clear it when we're called with nil, so this could go through the
// same code path more than once successfully.
[super setNextKeyView: aView ? documentView : nil];
} else
[super setNextKeyView:aView];
}
- (void)viewDidMoveToWindow
{
// FIXME 2573089: we can observe a notification for first responder changes
// instead of the very frequent NSWindowDidUpdateNotification if/when 2573089 is addressed.
NSWindow *newWindow = [self window];
if (!newWindow)
return;
[self _trackFirstResponder];
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:@selector(_trackFirstResponder)
name:NSWindowDidUpdateNotification
object:newWindow];
if (previewView)
[notificationCenter addObserver:self
selector:@selector(_receivedPDFKitLaunchNotification:)
name:PDFKitLaunchNotification
object:previewView];
}
- (void)viewWillMoveToWindow:(NSWindow *)window
{
// FIXME 2573089: we can observe a notification for changes to the first responder
// instead of the very frequent NSWindowDidUpdateNotification if/when 2573089 is addressed.
NSWindow *oldWindow = [self window];
if (!oldWindow)
return;
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter removeObserver:self
name:NSWindowDidUpdateNotification
object:oldWindow];
if (previewView)
[notificationCenter removeObserver:self
name:PDFKitLaunchNotification
object:previewView];
[trackedFirstResponder release];
trackedFirstResponder = nil;
}
#pragma mark NSUserInterfaceValidations PROTOCOL IMPLEMENTATION
- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)item
{
SEL action = [item action];
if (action == @selector(takeFindStringFromSelection:) || action == @selector(centerSelectionInVisibleArea:) || action == @selector(jumpToSelection:))
return [PDFSubview currentSelection] != nil;
if (action == @selector(_openWithFinder:))
return [PDFSubview document] != nil;
return YES;
}
#pragma mark INTERFACE BUILDER ACTIONS FOR SAFARI
// Surprisingly enough, this isn't defined in any superclass, though it is defined in assorted AppKit classes since
// it's a standard menu item IBAction.
- (IBAction)copy:(id)sender
{
[PDFSubview copy:sender];
}
// This used to be a standard IBAction (for Use Selection For Find), but AppKit now uses performFindPanelAction:
// with a menu item tag for this purpose.
- (IBAction)takeFindStringFromSelection:(id)sender
{
[NSPasteboard _web_setFindPasteboardString:[[PDFSubview currentSelection] string] withOwner:self];
}
#pragma mark WebFrameView UNDECLARED "DELEGATE METHODS"
// This is tested in -[WebFrameView canPrintHeadersAndFooters], but isn't declared anywhere (yuck)
- (BOOL)canPrintHeadersAndFooters
{
return NO;
}
// This is tested in -[WebFrameView printOperationWithPrintInfo:], but isn't declared anywhere (yuck)
- (NSPrintOperation *)printOperationWithPrintInfo:(NSPrintInfo *)printInfo
{
return [[PDFSubview document] getPrintOperationForPrintInfo:printInfo autoRotate:YES];
}
#pragma mark WebDocumentView PROTOCOL IMPLEMENTATION
- (void)setDataSource:(WebDataSource *)ds
{
dataSource = ds;
[self setFrame:[[self superview] frame]];
}
- (void)dataSourceUpdated:(WebDataSource *)dataSource
{
}
- (void)setNeedsLayout:(BOOL)flag
{
}
- (void)layout
{
}
- (void)viewWillMoveToHostWindow:(NSWindow *)hostWindow
{
}
- (void)viewDidMoveToHostWindow
{
}
#pragma mark WebDocumentElement PROTOCOL IMPLEMENTATION
- (NSDictionary *)elementAtPoint:(NSPoint)point
{
WebFrame *frame = [dataSource webFrame];
ASSERT(frame);
return [NSDictionary dictionaryWithObjectsAndKeys:
frame, WebElementFrameKey,
[NSNumber numberWithBool:[self _pointIsInSelection:point]], WebElementIsSelectedKey,
nil];
}
- (NSDictionary *)elementAtPoint: