71ef1caccfe74a0725dfdcc550c0c18ac26d1c35
[WebKit-https.git] / WebKit / WebView / WebPDFView.mm
1 /*
2  * Copyright (C) 2005, 2006 Apple Computer, 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  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import "WebPDFView.h"
30
31 #import "WebDataSourceInternal.h"
32 #import "WebDocumentInternal.h"
33 #import "WebDocumentPrivate.h"
34 #import "WebFrame.h"
35 #import "WebFrameInternal.h"
36 #import "WebFrameView.h"
37 #import "WebLocalizableStrings.h"
38 #import "WebNSAttributedStringExtras.h"
39 #import "WebNSPasteboardExtras.h"
40 #import "WebNSViewExtras.h"
41 #import "WebPDFRepresentation.h"
42 #import "WebPreferencesPrivate.h"
43 #import "WebUIDelegate.h"
44 #import "WebView.h"
45 #import "WebViewInternal.h"
46 #import <JavaScriptCore/Assertions.h>
47 #import <PDFKit/PDFKit.h>
48 #import <WebCore/FrameLoader.h>
49 #import <WebKitSystemInterface.h>
50
51 #define TEMP_PREFIX "/tmp/XXXXXX-"
52 #define OBJC_TEMP_PREFIX @"/tmp/XXXXXX-"
53
54 #define PDFKitLaunchNotification @"PDFPreviewLaunchPreview"
55
56 // QuartzPrivate.h doesn't include the PDFKit private headers, so we can't get at PDFViewPriv.h. (3957971)
57 // Even if that was fixed, we'd have to tweak compile options to include QuartzPrivate.h. (3957839)
58
59 @interface WebPDFView (FileInternal)
60 + (Class)_PDFPreviewViewClass;
61 + (Class)_PDFViewClass;
62 - (BOOL)_anyPDFTagsFoundInMenu:(NSMenu *)menu;
63 - (void)_applyPDFDefaults;
64 - (NSEvent *)_fakeKeyEventWithFunctionKey:(unichar)functionKey;
65 - (NSMutableArray *)_menuItemsFromPDFKitForEvent:(NSEvent *)theEvent;
66 - (void)_openWithFinder:(id)sender;
67 - (NSString *)_path;
68 - (PDFView *)_PDFSubview;
69 - (BOOL)_pointIsInSelection:(NSPoint)point;
70 - (void)_receivedPDFKitLaunchNotification:(NSNotification *)notification;
71 - (NSAttributedString *)_scaledAttributedString:(NSAttributedString *)unscaledAttributedString;
72 - (void)_trackFirstResponder;
73 @end;
74
75 // PDFPrefUpdatingProxy is a class that forwards everything it gets to a target and updates the PDF viewing prefs
76 // after each of those messages.  We use it as a way to hook all the places that the PDF viewing attrs change.
77 @interface PDFPrefUpdatingProxy : NSProxy {
78     WebPDFView *view;
79 }
80 - (id)initWithView:(WebPDFView *)view;
81 @end
82
83 @interface PDFDocument (PDFKitSecretsIKnow)
84 - (NSPrintOperation *)getPrintOperationForPrintInfo:(NSPrintInfo *)printInfo autoRotate:(BOOL)doRotate;
85 @end
86
87 extern "C" NSString *_NSPathForSystemFramework(NSString *framework);
88
89 #pragma mark C UTILITY FUNCTIONS
90
91 static void _applicationInfoForMIMEType(NSString *type, NSString **name, NSImage **image)
92 {
93     NSURL *appURL = nil;
94     
95     OSStatus error = LSCopyApplicationForMIMEType((CFStringRef)type, kLSRolesAll, (CFURLRef *)&appURL);
96     if (error != noErr)
97         return;
98     
99     NSString *appPath = [appURL path];
100     CFRelease (appURL);
101     
102     *image = [[NSWorkspace sharedWorkspace] iconForFile:appPath];  
103     [*image setSize:NSMakeSize(16.f,16.f)];  
104     
105     NSString *appName = [[NSFileManager defaultManager] displayNameAtPath:appPath];
106     *name = appName;
107 }
108
109 // FIXME 4182876: We can eliminate this function in favor if -isEqual: if [PDFSelection isEqual:] is overridden
110 // to compare contents.
111 static BOOL _PDFSelectionsAreEqual(PDFSelection *selectionA, PDFSelection *selectionB)
112 {
113     NSArray *aPages = [selectionA pages];
114     NSArray *bPages = [selectionB pages];
115     
116     if (![aPages isEqual:bPages])
117         return NO;
118     
119     int count = [aPages count];
120     int i;
121     for (i = 0; i < count; ++i) {
122         NSRect aBounds = [selectionA boundsForPage:[aPages objectAtIndex:i]];
123         NSRect bBounds = [selectionB boundsForPage:[bPages objectAtIndex:i]];
124         if (!NSEqualRects(aBounds, bBounds)) {
125             return NO;
126         }
127     }
128     
129     return YES;
130 }
131
132 @implementation WebPDFView
133
134 #pragma mark WebPDFView API
135
136 + (NSBundle *)PDFKitBundle
137 {
138     static NSBundle *PDFKitBundle = nil;
139     if (PDFKitBundle == nil) {
140         NSString *PDFKitPath = [_NSPathForSystemFramework(@"Quartz.framework") stringByAppendingString:@"/Frameworks/PDFKit.framework"];
141         if (PDFKitPath == nil) {
142             LOG_ERROR("Couldn't find PDFKit.framework");
143             return nil;
144         }
145         PDFKitBundle = [NSBundle bundleWithPath:PDFKitPath];
146         if (![PDFKitBundle load]) {
147             LOG_ERROR("Couldn't load PDFKit.framework");
148         }
149     }
150     return PDFKitBundle;
151 }
152
153 + (NSArray *)supportedMIMETypes
154 {
155     return [WebPDFRepresentation supportedMIMETypes];
156 }
157
158 - (void)setPDFDocument:(PDFDocument *)doc
159 {
160     [PDFSubview setDocument:doc];
161     [self _applyPDFDefaults];
162 }
163
164 #pragma mark NSObject OVERRIDES
165
166 - (void)dealloc
167 {
168     ASSERT(!trackedFirstResponder);
169     [previewView release];
170     [PDFSubview release];
171     [path release];
172     [PDFSubviewProxy release];
173     [super dealloc];
174 }
175
176 #pragma mark NSResponder OVERRIDES
177
178 - (void)centerSelectionInVisibleArea:(id)sender
179 {
180     [PDFSubview scrollSelectionToVisible:nil];
181 }
182
183 - (void)scrollPageDown:(id)sender
184 {
185     // PDFView doesn't support this responder method directly, so we pass it a fake key event
186     [PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSPageDownFunctionKey]];
187 }
188
189 - (void)scrollPageUp:(id)sender
190 {
191     // PDFView doesn't support this responder method directly, so we pass it a fake key event
192     [PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSPageUpFunctionKey]];
193 }
194
195 - (void)scrollLineDown:(id)sender
196 {
197     // PDFView doesn't support this responder method directly, so we pass it a fake key event
198     [PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSDownArrowFunctionKey]];
199 }
200
201 - (void)scrollLineUp:(id)sender
202 {
203     // PDFView doesn't support this responder method directly, so we pass it a fake key event
204     [PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSUpArrowFunctionKey]];
205 }
206
207 - (void)scrollToBeginningOfDocument:(id)sender
208 {
209     // PDFView doesn't support this responder method directly, so we pass it a fake key event
210     [PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSHomeFunctionKey]];
211 }
212
213 - (void)scrollToEndOfDocument:(id)sender
214 {
215     // PDFView doesn't support this responder method directly, so we pass it a fake key event
216     [PDFSubview keyDown:[self _fakeKeyEventWithFunctionKey:NSEndFunctionKey]];
217 }
218
219 // jumpToSelection is the old name for what AppKit now calls centerSelectionInVisibleArea. Safari
220 // was using the old jumpToSelection selector in its menu. Newer versions of Safari will us the
221 // selector centerSelectionInVisibleArea. We'll leave this old selector in place for two reasons:
222 // (1) compatibility between older Safari and newer WebKit; (2) other WebKit-based applications
223 // might be using the jumpToSelection: selector, and we don't want to break them.
224 - (void)jumpToSelection:(id)sender
225 {
226     [self centerSelectionInVisibleArea:nil];
227 }
228
229 #pragma mark NSView OVERRIDES
230
231 - (BOOL)acceptsFirstResponder {
232     return YES;
233 }
234
235 - (BOOL)becomeFirstResponder
236 {
237     // This works together with setNextKeyView to splice our PDFSubview into
238     // the key loop similar to the way NSScrollView does this.
239     NSWindow *window = [self window];
240     id newFirstResponder = nil;
241     
242     if ([window keyViewSelectionDirection] == NSSelectingPrevious) {
243         NSView *previousValidKeyView = [self previousValidKeyView];
244         if ((previousValidKeyView != self) && (previousValidKeyView != PDFSubview))
245             newFirstResponder = previousValidKeyView;
246     } else {
247         NSView *PDFDocumentView = [PDFSubview documentView];
248         if ([PDFDocumentView acceptsFirstResponder])
249             newFirstResponder = PDFDocumentView;
250     }
251     
252     if (!newFirstResponder)
253         return NO;
254     
255     if (![window makeFirstResponder:newFirstResponder])
256         return NO;
257     
258     [[dataSource webFrame] _clearSelectionInOtherFrames];
259     
260     return YES;
261 }
262
263 - (NSView *)hitTest:(NSPoint)point
264 {
265     // Override hitTest so we can override menuForEvent.
266     NSEvent *event = [NSApp currentEvent];
267     NSEventType type = [event type];
268     if (type == NSRightMouseDown || (type == NSLeftMouseDown && ([event modifierFlags] & NSControlKeyMask)))
269         return self;
270
271     return [super hitTest:point];
272 }
273
274 - (id)initWithFrame:(NSRect)frame
275 {
276     self = [super initWithFrame:frame];
277     if (self) {
278         [self setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
279         
280         Class previewViewClass = nil;
281         if ([[WebPreferences standardPreferences] _usePDFPreviewView])
282             previewViewClass = [[self class] _PDFPreviewViewClass];
283         
284         // We might not have found a previewViewClass even if we looked for one.
285         // But if we found the class we should be able to create an instance.
286         if (previewViewClass) {
287             previewView = [[previewViewClass alloc] initWithFrame:frame];
288             ASSERT(previewView);
289         }
290         
291         NSView *topLevelPDFKitView = nil;
292         if (previewView) {
293             PDFSubview = [previewView performSelector:@selector(pdfView)];
294             topLevelPDFKitView = previewView;
295         } else {
296             PDFSubview = [[[[self class] _PDFViewClass] alloc] initWithFrame:frame];
297             topLevelPDFKitView = PDFSubview;
298         }
299         
300         ASSERT(PDFSubview);
301         
302         [topLevelPDFKitView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
303         [self addSubview:topLevelPDFKitView];
304         
305         [PDFSubview setDelegate:self];
306         written = NO;
307         // Messaging this proxy is the same as messaging PDFSubview, with the side effect that the
308         // PDF viewing defaults are updated afterwards
309         PDFSubviewProxy = (PDFView *)[[PDFPrefUpdatingProxy alloc] initWithView:self];
310     }
311     
312     return self;
313 }
314
315 - (NSMenu *)menuForEvent:(NSEvent *)theEvent
316 {
317     // Start with the menu items supplied by PDFKit, with WebKit tags applied
318     NSMutableArray *items = [self _menuItemsFromPDFKitForEvent:theEvent];
319     
320     // Add in an "Open with <default PDF viewer>" item
321     NSString *appName = nil;
322     NSImage *appIcon = nil;
323     
324     _applicationInfoForMIMEType([[dataSource response] MIMEType], &appName, &appIcon);
325     if (!appName)
326         appName = UI_STRING("Finder", "Default application name for Open With context menu");
327     
328     NSString *title = [NSString stringWithFormat:UI_STRING("Open with %@", "context menu item for PDF"), appName];
329     NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:@selector(_openWithFinder:) keyEquivalent:@""];
330     [item setTag:WebMenuItemTagOpenWithDefaultApplication];
331     if (appIcon)
332         [item setImage:appIcon];
333     [items insertObject:item atIndex:0];
334     [item release];
335     
336     [items insertObject:[NSMenuItem separatorItem] atIndex:1];
337     
338     // pass the items off to the WebKit context menu mechanism
339     WebView *webView = [[dataSource webFrame] webView];
340     ASSERT(webView);
341     NSMenu *menu = [webView _menuForElement:[self elementAtPoint:[self convertPoint:[theEvent locationInWindow] fromView:nil]] defaultItems:items];
342     
343     // The delegate has now had the opportunity to add items to the standard PDF-related items, or to
344     // remove or modify some of the PDF-related items. In 10.4, the PDF context menu did not go through 
345     // the standard WebKit delegate path, and so the standard PDF-related items always appeared. For
346     // clients that create their own context menu by hand-picking specific items from the default list, such as
347     // Safari, none of the PDF-related items will appear until the client is rewritten to explicitly
348     // include these items. For backwards compatibility of tip-of-tree WebKit with the 10.4 version of Safari
349     // (the configuration that people building open source WebKit use), we'll use the entire set of PDFKit-supplied
350     // menu items. This backward-compatibility hack won't work with any non-Safari clients, but this seems OK since
351     // (1) the symptom is fairly minor, and (2) we suspect that non-Safari clients are probably using the entire
352     // set of default items, rather than manually choosing from them. We can remove this code entirely when we
353     // ship a version of Safari that includes the fix for radar 3796579.
354     if (![self _anyPDFTagsFoundInMenu:menu] && [[[NSBundle mainBundle] bundleIdentifier] isEqualToString:@"com.apple.Safari"]) {
355         [menu addItem:[NSMenuItem separatorItem]];
356         NSEnumerator *e = [items objectEnumerator];
357         NSMenuItem *menuItem;
358         while ((menuItem = [e nextObject]) != nil) {
359             // copy menuItem since a given menuItem can be in only one menu at a time, and we don't
360             // want to mess with the menu returned from PDFKit.
361             [menu addItem:[menuItem copy]];
362         }
363     }
364     
365     return menu;
366 }
367
368 - (void)setNextKeyView:(NSView *)aView
369 {
370     // This works together with becomeFirstResponder to splice PDFSubview into
371     // the key loop similar to the way NSScrollView and NSClipView do this.
372     NSView *documentView = [PDFSubview documentView];
373     if (documentView) {
374         [documentView setNextKeyView:aView];
375         
376         // We need to make the documentView be the next view in the keyview loop.
377         // It would seem more sensible to do this in our init method, but it turns out
378         // that [NSClipView setDocumentView] won't call this method if our next key view
379         // is already set, so we wait until we're called before adding this connection.
380         // We'll also clear it when we're called with nil, so this could go through the
381         // same code path more than once successfully.
382         [super setNextKeyView: aView ? documentView : nil];
383     } else
384         [super setNextKeyView:aView];
385 }
386
387 - (void)viewDidMoveToWindow
388 {
389     // FIXME 2573089: we can observe a notification for first responder changes
390     // instead of the very frequent NSWindowDidUpdateNotification if/when 2573089 is addressed.
391     NSWindow *newWindow = [self window];
392     if (!newWindow)
393         return;
394     
395     [self _trackFirstResponder];
396     NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
397     [notificationCenter addObserver:self
398                            selector:@selector(_trackFirstResponder) 
399                                name:NSWindowDidUpdateNotification
400                              object:newWindow];
401     
402     if (previewView)
403         [notificationCenter addObserver:self
404                                selector:@selector(_receivedPDFKitLaunchNotification:)
405                                    name:PDFKitLaunchNotification
406                                  object:previewView];
407 }
408
409 - (void)viewWillMoveToWindow:(NSWindow *)window
410 {
411     // FIXME 2573089: we can observe a notification for changes to the first responder
412     // instead of the very frequent NSWindowDidUpdateNotification if/when 2573089 is addressed.
413     NSWindow *oldWindow = [self window];
414     if (!oldWindow)
415         return;
416     
417     NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
418     [notificationCenter removeObserver:self
419                                   name:NSWindowDidUpdateNotification
420                                 object:oldWindow];
421     if (previewView)
422         [notificationCenter removeObserver:self
423                                       name:PDFKitLaunchNotification
424                                     object:previewView];
425     
426     [trackedFirstResponder release];
427     trackedFirstResponder = nil;
428 }
429
430 #pragma mark NSUserInterfaceValidations PROTOCOL IMPLEMENTATION
431
432 - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)item 
433 {
434     SEL action = [item action];    
435     if (action == @selector(takeFindStringFromSelection:) || action == @selector(centerSelectionInVisibleArea:) || action == @selector(jumpToSelection:))
436         return [PDFSubview currentSelection] != nil;
437
438     return YES;
439 }
440
441 #pragma mark INTERFACE BUILDER ACTIONS FOR SAFARI
442
443 // Surprisingly enough, this isn't defined in any superclass, though it is defined in assorted AppKit classes since
444 // it's a standard menu item IBAction.
445 - (IBAction)copy:(id)sender
446 {
447     [PDFSubview copy:sender];
448 }
449
450 // This used to be a standard IBAction (for Use Selection For Find), but AppKit now uses performFindPanelAction:
451 // with a menu item tag for this purpose.
452 - (IBAction)takeFindStringFromSelection:(id)sender
453 {
454     [NSPasteboard _web_setFindPasteboardString:[[PDFSubview currentSelection] string] withOwner:self];
455 }
456
457 #pragma mark WebFrameView UNDECLARED "DELEGATE METHODS"
458
459 // This is tested in -[WebFrameView canPrintHeadersAndFooters], but isn't declared anywhere (yuck)
460 - (BOOL)canPrintHeadersAndFooters
461 {
462     return NO;
463 }
464
465 // This is tested in -[WebFrameView printOperationWithPrintInfo:], but isn't declared anywhere (yuck)
466 - (NSPrintOperation *)printOperationWithPrintInfo:(NSPrintInfo *)printInfo
467 {
468     return [[PDFSubview document] getPrintOperationForPrintInfo:printInfo autoRotate:YES];
469 }
470
471 #pragma mark WebDocumentView PROTOCOL IMPLEMENTATION
472
473 - (void)setDataSource:(WebDataSource *)ds
474 {
475     dataSource = ds;
476     [self setFrame:[[self superview] frame]];
477 }
478
479 - (void)dataSourceUpdated:(WebDataSource *)dataSource
480 {
481 }
482
483 - (void)setNeedsLayout:(BOOL)flag
484 {
485 }
486
487 - (void)layout
488 {
489 }
490
491 - (void)viewWillMoveToHostWindow:(NSWindow *)hostWindow
492 {
493 }
494
495 - (void)viewDidMoveToHostWindow
496 {
497 }
498
499 #pragma mark WebDocumentElement PROTOCOL IMPLEMENTATION
500
501 - (NSDictionary *)elementAtPoint:(NSPoint)point
502 {
503     WebFrame *frame = [dataSource webFrame];
504     ASSERT(frame);
505     
506     return [NSDictionary dictionaryWithObjectsAndKeys:
507         frame, WebElementFrameKey, 
508         [NSNumber numberWithBool:[self _pointIsInSelection:point]], WebElementIsSelectedKey,
509         nil];
510 }
511
512 - (NSDictionary *)elementAtPoint:(NSPoint)point allowShadowContent:(BOOL)allow
513 {
514     return [self elementAtPoint:point];
515 }
516
517 #pragma mark WebDocumentSearching PROTOCOL IMPLEMENTATION
518
519 - (BOOL)searchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag
520 {
521     if (![string length])
522         return NO;
523
524     // Our search algorithm, used in WebCore also, is to search in the selection first. If the found text is the
525     // entire selection, then we search again from just past the selection.
526
527     int options = 0;
528     if (!forward)
529         options |= NSBackwardsSearch;
530
531     if (!caseFlag)
532         options |= NSCaseInsensitiveSearch;
533
534     PDFDocument *document = [PDFSubview document];
535     PDFSelection *oldSelection = [PDFSubview currentSelection];
536     
537     // Initially we want to include the selected text in the search. PDFDocument's API always searches from just
538     // past the passed-in selection, so we need to pass a selection that's modified appropriately. 
539     // FIXME 4182863: Ideally we'd use a zero-length selection at the edge of the current selection, but zero-length
540     // selections don't work in PDFDocument. So instead we make a one-length selection just before or after the
541     // current selection, which works for our purposes even when the current selection is at an edge of the
542     // document.
543     PDFSelection *selectionForInitialSearch = [oldSelection copy];
544     int oldSelectionLength = [[oldSelection string] length];
545     if (forward) {
546         [selectionForInitialSearch extendSelectionAtStart:1];
547         [selectionForInitialSearch extendSelectionAtEnd:-oldSelectionLength];
548     } else {
549         [selectionForInitialSearch extendSelectionAtEnd:1];
550         [selectionForInitialSearch extendSelectionAtStart:-oldSelectionLength];
551     }
552     PDFSelection *foundSelection = [document findString:string fromSelection:selectionForInitialSearch withOptions:options];
553     [selectionForInitialSearch release];
554     
555     // If we found the selection, search again from just past the selection
556     if (_PDFSelectionsAreEqual(foundSelection, oldSelection))
557         foundSelection = [document findString:string fromSelection:oldSelection withOptions:options];
558     
559     if (!foundSelection && wrapFlag)
560         foundSelection = [document findString:string fromSelection:nil withOptions:options];
561
562     if (foundSelection) {
563         [PDFSubview setCurrentSelection:foundSelection];
564         [PDFSubview scrollSelectionToVisible:nil];
565         return YES;
566     }
567     return NO;
568 }
569
570 #pragma mark WebDocumentText PROTOCOL IMPLEMENTATION
571
572 - (BOOL)supportsTextEncoding
573 {
574     return NO;
575 }
576
577 - (NSString *)string
578 {
579     return [[PDFSubview document] string];
580 }
581
582 - (NSAttributedString *)attributedString
583 {
584     // changing the selection is a hack, but the only way to get an attr string is via PDFSelection
585     
586     // must copy this selection object because we change the selection which seems to release it
587     PDFSelection *savedSelection = [[PDFSubview currentSelection] copy];
588     [PDFSubview selectAll:nil];
589     NSAttributedString *result = [[PDFSubview currentSelection] attributedString];
590     if (savedSelection) {
591         [PDFSubview setCurrentSelection:savedSelection];
592         [savedSelection release];
593     } else {
594         // FIXME: behavior of setCurrentSelection:nil is not documented - check 4182934 for progress
595         // Otherwise, we could collapse this code with the case above.
596         [PDFSubview clearSelection];
597     }
598     
599     result = [self _scaledAttributedString:result];
600     
601     return result;
602 }
603
604 - (NSString *)selectedString
605 {
606     return [[PDFSubview currentSelection] string];
607 }
608
609 - (NSAttributedString *)selectedAttributedString
610 {
611     return [self _scaledAttributedString:[[PDFSubview currentSelection] attributedString]];
612 }
613
614 - (void)selectAll
615 {
616     [PDFSubview selectAll:nil];
617 }
618
619 - (void)deselectAll
620 {
621     [PDFSubview clearSelection];
622 }
623
624 #pragma mark WebDocumentViewState PROTOCOL IMPLEMENTATION
625
626 // Even though to WebKit we are the "docView", in reality a PDFView contains its own scrollview and docView.
627 // And it even turns out there is another PDFKit view between the docView and its enclosing ScrollView, so
628 // we have to be sure to do our calculations based on that view, immediately inside the ClipView.  We try
629 // to make as few assumptions about the PDFKit view hierarchy as possible.
630
631 - (NSPoint)scrollPoint
632 {
633     NSView *realDocView = [PDFSubview documentView];
634     NSClipView *clipView = [[realDocView enclosingScrollView] contentView];
635     return [clipView bounds].origin;
636 }
637
638 - (void)setScrollPoint:(NSPoint)p
639 {
640     WebFrame *frame = [dataSource webFrame];
641     //FIXME:  We only restore scroll state in the non-frames case because otherwise we get a crash due to
642     // PDFKit calling display from within its drawRect:. See bugzilla 4164.
643     if (![frame parentFrame]) {
644         NSView *realDocView = [PDFSubview documentView];
645         [[[realDocView enclosingScrollView] documentView] scrollPoint:p];
646     }
647 }
648
649 - (id)viewState
650 {
651     NSMutableArray *state = [NSMutableArray arrayWithCapacity:4];
652     PDFDisplayMode mode = [PDFSubview displayMode];
653     [state addObject:[NSNumber numberWithInt:mode]];
654     if (mode == kPDFDisplaySinglePage || mode == kPDFDisplayTwoUp) {
655         unsigned int pageIndex = [[PDFSubview document] indexForPage:[PDFSubview currentPage]];
656         [state addObject:[NSNumber numberWithUnsignedInt:pageIndex]];
657     }  // else in continuous modes, scroll position gets us to the right page
658     BOOL autoScaleFlag = [PDFSubview autoScales];
659     [state addObject:[NSNumber numberWithBool:autoScaleFlag]];
660     if (!autoScaleFlag)
661         [state addObject:[NSNumber numberWithFloat:[PDFSubview scaleFactor]]];
662
663     return state;
664 }
665
666 - (void)setViewState:(id)statePList
667 {
668     ASSERT([statePList isKindOfClass:[NSArray class]]);
669     NSArray *state = statePList;
670     int i = 0;
671     PDFDisplayMode mode = [[state objectAtIndex:i++] intValue];
672     [PDFSubview setDisplayMode:mode];
673     if (mode == kPDFDisplaySinglePage || mode == kPDFDisplayTwoUp) {
674         unsigned int pageIndex = [[state objectAtIndex:i++] unsignedIntValue];
675         [PDFSubview goToPage:[[PDFSubview document] pageAtIndex:pageIndex]];
676     }  // else in continuous modes, scroll position gets us to the right page
677     BOOL autoScaleFlag = [[state objectAtIndex:i++] boolValue];
678     [PDFSubview setAutoScales:autoScaleFlag];
679     if (!autoScaleFlag)
680         [PDFSubview setScaleFactor:[[state objectAtIndex:i++] floatValue]];
681 }
682
683 #pragma mark _WebDocumentTextSizing PROTOCOL IMPLEMENTATION
684
685 - (IBAction)_makeTextSmaller:(id)sender
686 {
687     [PDFSubviewProxy zoomOut:sender];
688 }
689
690 - (IBAction)_makeTextLarger:(id)sender
691 {
692     [PDFSubviewProxy zoomIn:sender];
693 }
694
695 - (IBAction)_makeTextStandardSize:(id)sender
696 {
697     [PDFSubviewProxy setScaleFactor:1.0f];
698 }
699
700 // never sent because we do not track the common size factor
701 - (void)_textSizeMultiplierChanged      { ASSERT_NOT_REACHED(); }
702
703 - (BOOL)_tracksCommonSizeFactor
704 {
705     // We keep our own scale factor instead of tracking the common one in the WebView for a couple reasons.
706     // First, PDFs tend to have visually smaller text because they are laid out for a printed page instead of
707     // the screen.  Second, the PDFView feature of AutoScaling means our scaling factor can be quiet variable.
708     return NO;
709 }
710
711 - (BOOL)_canMakeTextSmaller
712 {
713     return [PDFSubview canZoomOut];
714 }
715
716 - (BOOL)_canMakeTextLarger
717 {
718     return [PDFSubview canZoomIn];
719 }
720
721 - (BOOL)_canMakeTextStandardSize
722 {
723     return [PDFSubview scaleFactor] != 1.0;
724 }
725
726 #pragma mark WebDocumentSelection PROTOCOL IMPLEMENTATION
727
728 - (NSRect)selectionRect
729 {
730     NSRect result = NSZeroRect;
731     PDFSelection *selection = [PDFSubview currentSelection];
732     NSEnumerator *pages = [[selection pages] objectEnumerator];
733     PDFPage *page;
734     while ((page = [pages nextObject]) != nil) {
735         NSRect selectionOnPageInPDFViewCoordinates = [PDFSubview convertRect:[selection boundsForPage:page] fromPage:page];
736         if (NSIsEmptyRect(result))
737             result = selectionOnPageInPDFViewCoordinates;
738         else
739             result = NSUnionRect(result, selectionOnPageInPDFViewCoordinates);
740     }
741     
742     // Convert result to be in documentView (selectionView) coordinates
743     result = [PDFSubview convertRect:result toView:[PDFSubview documentView]];
744     
745     return result;
746 }
747
748 - (NSView *)selectionView
749 {
750     return [PDFSubview documentView];
751 }
752
753 - (NSImage *)selectionImageForcingWhiteText:(BOOL)forceWhiteText
754 {
755     // Convert the selection to an attributed string, and draw that.
756     // FIXME 4621154: this doesn't handle italics (and maybe other styles)
757     // FIXME 4604366: this doesn't handle text at non-actual size
758     NSMutableAttributedString *attributedString = [[self selectedAttributedString] mutableCopy];
759     NSRange wholeStringRange = NSMakeRange(0, [attributedString length]);
760     
761     // Modify the styles in the attributed string to draw white text, no background, and no underline. We draw 
762     // no underline because it would look ugly.
763     [attributedString beginEditing];
764     [attributedString removeAttribute:NSBackgroundColorAttributeName range:wholeStringRange];
765     [attributedString removeAttribute:NSUnderlineStyleAttributeName range:wholeStringRange];
766     if (forceWhiteText)
767         [attributedString addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithDeviceWhite:1.0f alpha:1.0f] range:wholeStringRange];
768     [attributedString endEditing];
769     
770     NSImage* selectionImage = [[[NSImage alloc] initWithSize:[self selectionImageRect].size] autorelease];
771     
772     [selectionImage lockFocus];
773     [attributedString drawAtPoint:NSZeroPoint];
774     [selectionImage unlockFocus];
775     
776     [attributedString release];
777
778     return selectionImage;
779 }
780
781 - (NSRect)selectionImageRect
782 {
783     // FIXME: deal with clipping?
784     return [self selectionRect];
785 }
786
787 - (NSArray *)pasteboardTypesForSelection
788 {
789     return [NSArray arrayWithObjects:NSRTFDPboardType, NSRTFPboardType, NSStringPboardType, nil];
790 }
791
792 - (void)writeSelectionWithPasteboardTypes:(NSArray *)types toPasteboard:(NSPasteboard *)pasteboard
793 {
794     NSAttributedString *attributedString = [self selectedAttributedString];
795     
796     if ([types containsObject:NSRTFDPboardType]) {
797         NSData *RTFDData = [attributedString RTFDFromRange:NSMakeRange(0, [attributedString length]) documentAttributes:nil];
798         [pasteboard setData:RTFDData forType:NSRTFDPboardType];
799     }        
800     
801     if ([types containsObject:NSRTFPboardType]) {
802         if ([attributedString containsAttachments])
803             attributedString = [attributedString _web_attributedStringByStrippingAttachmentCharacters];
804
805         NSData *RTFData = [attributedString RTFFromRange:NSMakeRange(0, [attributedString length]) documentAttributes:nil];
806         [pasteboard setData:RTFData forType:NSRTFPboardType];
807     }
808     
809     if ([types containsObject:NSStringPboardType])
810         [pasteboard setString:[self selectedString] forType:NSStringPboardType];
811 }
812
813 #pragma mark PDFView DELEGATE METHODS
814
815 // Delegates implementing the following method will be called to handle clicks on URL
816 // links within the PDFView.  
817 - (void)PDFViewWillClickOnLink:(PDFView *)sender withURL:(NSURL *)URL
818 {
819     if (URL)
820         [[dataSource webFrame] _frameLoader]->safeLoad(URL);
821 }
822
823 @end
824
825 @implementation WebPDFView (FileInternal)
826
827 + (Class)_PDFPreviewViewClass
828 {
829     static Class PDFPreviewViewClass = nil;
830     static BOOL checkedForPDFPreviewViewClass = NO;
831     
832     if (!checkedForPDFPreviewViewClass) {
833         checkedForPDFPreviewViewClass = YES;
834         PDFPreviewViewClass = [[WebPDFView PDFKitBundle] classNamed:@"PDFPreviewView"];
835     }
836     
837     // This class might not be available; callers need to deal with a nil return here.
838     return PDFPreviewViewClass;
839 }
840
841 + (Class)_PDFViewClass
842 {
843     static Class PDFViewClass = nil;
844     if (PDFViewClass == nil) {
845         PDFViewClass = [[WebPDFView PDFKitBundle] classNamed:@"PDFView"];
846         if (!PDFViewClass)
847             LOG_ERROR("Couldn't find PDFView class in PDFKit.framework");
848     }
849     return PDFViewClass;
850 }
851
852 - (BOOL)_anyPDFTagsFoundInMenu:(NSMenu *)menu
853 {
854     NSEnumerator *e = [[menu itemArray] objectEnumerator];
855     NSMenuItem *item;
856     while ((item = [e nextObject]) != nil) {
857         switch ([item tag]) {
858             case WebMenuItemTagOpenWithDefaultApplication:
859             case WebMenuItemPDFActualSize:
860             case WebMenuItemPDFZoomIn:
861             case WebMenuItemPDFZoomOut:
862             case WebMenuItemPDFAutoSize:
863             case WebMenuItemPDFSinglePage:
864             case WebMenuItemPDFFacingPages:
865             case WebMenuItemPDFContinuous:
866             case WebMenuItemPDFNextPage:
867             case WebMenuItemPDFPreviousPage:
868                 return YES;
869         }
870     }
871     return NO;
872 }
873
874 - (void)_applyPDFDefaults
875 {
876     // Set up default viewing params
877     WebPreferences *prefs = [[dataSource _webView] preferences];
878     float scaleFactor = [prefs PDFScaleFactor];
879     if (scaleFactor == 0)
880         [PDFSubview setAutoScales:YES];
881     else {
882         [PDFSubview setAutoScales:NO];
883         [PDFSubview setScaleFactor:scaleFactor];
884     }
885     [PDFSubview setDisplayMode:[prefs PDFDisplayMode]];    
886 }
887
888 - (NSEvent *)_fakeKeyEventWithFunctionKey:(unichar)functionKey
889 {
890     // FIXME 4400480: when PDFView implements the standard scrolling selectors that this
891     // method is used to mimic, we can eliminate this method and call them directly.
892     NSString *keyAsString = [NSString stringWithCharacters:&functionKey length:1];
893     return [NSEvent keyEventWithType:NSKeyDown
894                             location:NSZeroPoint
895                        modifierFlags:0
896                            timestamp:0
897                         windowNumber:0
898                              context:nil
899                           characters:keyAsString
900          charactersIgnoringModifiers:keyAsString
901                            isARepeat:NO
902                              keyCode:0];
903 }
904
905 - (NSMutableArray *)_menuItemsFromPDFKitForEvent:(NSEvent *)theEvent
906 {
907     NSMutableArray *copiedItems = [NSMutableArray array];
908     NSDictionary *actionsToTags = [[NSDictionary alloc] initWithObjectsAndKeys:
909         [NSNumber numberWithInt:WebMenuItemPDFActualSize], NSStringFromSelector(@selector(_setActualSize:)),
910         [NSNumber numberWithInt:WebMenuItemPDFZoomIn], NSStringFromSelector(@selector(zoomIn:)),
911         [NSNumber numberWithInt:WebMenuItemPDFZoomOut], NSStringFromSelector(@selector(zoomOut:)),
912         [NSNumber numberWithInt:WebMenuItemPDFAutoSize], NSStringFromSelector(@selector(_setAutoSize:)),
913         [NSNumber numberWithInt:WebMenuItemPDFSinglePage], NSStringFromSelector(@selector(_setSinglePage:)),
914         [NSNumber numberWithInt:WebMenuItemPDFFacingPages], NSStringFromSelector(@selector(_setDoublePage:)),
915         [NSNumber numberWithInt:WebMenuItemPDFContinuous], NSStringFromSelector(@selector(_toggleContinuous:)),
916         [NSNumber numberWithInt:WebMenuItemPDFNextPage], NSStringFromSelector(@selector(goToNextPage:)),
917         [NSNumber numberWithInt:WebMenuItemPDFPreviousPage], NSStringFromSelector(@selector(goToPreviousPage:)),
918         nil];
919     
920     NSEnumerator *e = [[[PDFSubview menuForEvent:theEvent] itemArray] objectEnumerator];
921     NSMenuItem *item;
922     while ((item = [e nextObject]) != nil) {
923         // Copy items since a menu item can be in only one menu at a time, and we don't
924         // want to modify the original menu supplied by PDFKit.
925         NSMenuItem *itemCopy = [item copy];
926         [copiedItems addObject:itemCopy];
927         
928         if ([itemCopy isSeparatorItem])
929             continue;
930
931         NSString *actionString = NSStringFromSelector([itemCopy action]);
932         NSNumber *tagNumber = [actionsToTags objectForKey:actionString];
933         
934         int tag;
935         if (tagNumber != nil)
936             tag = [tagNumber intValue];
937         else {
938             tag = WebMenuItemTagOther;
939             LOG_ERROR("no WebKit menu item tag found for PDF context menu item action \"%@\", using WebMenuItemTagOther", actionString);
940         }
941         
942         if ([itemCopy tag] == 0) {
943             [itemCopy setTag:tag];
944             if ([itemCopy target] == PDFSubview) {
945                 // Note that updating the defaults is cheap because it catches redundant settings, so installing
946                 // the proxy for actions that don't impact the defaults is OK
947                 [itemCopy setTarget:PDFSubviewProxy];
948             }
949         } else
950             LOG_ERROR("PDF context menu item %@ came with tag %d, so no WebKit tag was applied. This could mean that the item doesn't appear in clients such as Safari.", [itemCopy title], [itemCopy tag]);
951     }
952     
953     [actionsToTags release];
954     
955     return copiedItems;
956 }
957
958 - (void)_openWithFinder:(id)sender
959 {
960     NSString *opath = [self _path];
961     
962     if (opath) {
963         if (!written) {
964             [[dataSource data] writeToFile:opath atomically:YES];
965             written = YES;
966         }
967         
968         if (![[NSWorkspace sharedWorkspace] openFile:opath]) {
969             // NSWorkspace couldn't open file.  Do we need an alert
970             // here?  We ignore the error elsewhere.
971         }
972     }
973 }
974
975 - (NSString *)_path
976 {
977     // Generate path once.
978     if (path)
979         return path;
980     
981     NSString *filename = [[dataSource response] suggestedFilename];
982     NSFileManager *manager = [NSFileManager defaultManager];    
983     
984     path = [@"/tmp/" stringByAppendingPathComponent: filename];
985     if ([manager fileExistsAtPath:path]) {
986         path = [OBJC_TEMP_PREFIX stringByAppendingString:filename];
987         char *cpath = (char *)[path fileSystemRepresentation];
988         
989         int fd = mkstemps (cpath, strlen(cpath) - strlen(TEMP_PREFIX) + 1);
990         if (fd < 0) {
991             // Couldn't create a temporary file!  Should never happen.  Do
992             // we need an alert here?
993             path = nil;
994         } else {
995             close (fd);
996             path = [manager stringWithFileSystemRepresentation:cpath length:strlen(cpath)];
997         }
998     }
999     
1000     [path retain];
1001     
1002     return path;
1003 }
1004
1005 - (PDFView *)_PDFSubview
1006 {
1007     return PDFSubview;
1008 }
1009
1010 - (BOOL)_pointIsInSelection:(NSPoint)point
1011 {
1012     PDFPage *page = [PDFSubview pageForPoint:point nearest:NO];
1013     if (!page)
1014         return NO;
1015     
1016     NSRect selectionRect = [PDFSubview convertRect:[[PDFSubview currentSelection] boundsForPage:page] fromPage:page];
1017     
1018     return NSPointInRect(point, selectionRect);
1019 }
1020
1021 - (void)_receivedPDFKitLaunchNotification:(NSNotification *)notification
1022 {
1023     ASSERT([notification object] == previewView);
1024     [self _openWithFinder:self];
1025 }
1026
1027 - (NSAttributedString *)_scaledAttributedString:(NSAttributedString *)unscaledAttributedString
1028 {
1029     NSLog(@"unscaledAttributedString is %@", unscaledAttributedString);
1030     if (!unscaledAttributedString)
1031         return nil;
1032     
1033     float scaleFactor = [PDFSubview scaleFactor];
1034     if (scaleFactor == 1.0)
1035         return unscaledAttributedString;
1036     
1037     NSMutableAttributedString *result = [[unscaledAttributedString mutableCopy] autorelease];
1038     unsigned int length = [result length];
1039     NSRange effectiveRange = NSMakeRange(0,0);
1040     
1041     [result beginEditing];    
1042     while (NSMaxRange(effectiveRange) < length) {
1043         NSFont *unscaledFont = [result attribute:NSFontAttributeName atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange];
1044         NSFont *scaledFont = [NSFont fontWithName:[unscaledFont fontName] size:[unscaledFont pointSize]*scaleFactor];
1045         [result addAttribute:NSFontAttributeName value:scaledFont range:effectiveRange];
1046     }
1047     [result endEditing];
1048     
1049     return result;
1050 }
1051
1052 - (void)_trackFirstResponder
1053 {
1054     ASSERT([self window]);
1055     
1056     id newFirstResponder = [[self window] firstResponder];
1057     if (newFirstResponder == trackedFirstResponder)
1058         return;
1059     
1060     // This next clause is the entire purpose of _trackFirstResponder. In other WebDocument
1061     // view classes this is done in a resignFirstResponder override, but in this case the
1062     // first responder view is a PDFKit class that we can't subclass.
1063     if (trackedFirstResponder == [PDFSubview documentView] && ![[dataSource _webView] maintainsInactiveSelection])
1064         [self deselectAll];
1065     
1066     
1067     [trackedFirstResponder release];
1068     trackedFirstResponder = [newFirstResponder retain];
1069 }
1070
1071 @end;
1072
1073 @implementation PDFPrefUpdatingProxy
1074
1075 - (id)initWithView:(WebPDFView *)aView
1076 {
1077     // No [super init], since we inherit from NSProxy
1078     view = aView;
1079     return self;
1080 }
1081
1082 - (void)forwardInvocation:(NSInvocation *)invocation
1083 {
1084     PDFView *PDFSubview = [view _PDFSubview];
1085     [invocation invokeWithTarget:PDFSubview];
1086
1087     WebPreferences *prefs = [[view->dataSource _webView] preferences];
1088     float scaleFactor = [PDFSubview autoScales] ? 0.0f : [PDFSubview scaleFactor];
1089     [prefs setPDFScaleFactor:scaleFactor];
1090     [prefs setPDFDisplayMode:[PDFSubview displayMode]];
1091 }
1092
1093 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
1094 {
1095     return [[view _PDFSubview] methodSignatureForSelector:sel];
1096 }
1097
1098 @end