Reviewed by Adam Roben
[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             // We'll retain the PDFSubview here so that it is equally retained in all
294             // code paths. That way we don't need to worry about conditionally releasing
295             // it later.
296             PDFSubview = [[previewView performSelector:@selector(pdfView)] retain];
297             topLevelPDFKitView = previewView;
298         } else {
299             PDFSubview = [[[[self class] _PDFViewClass] alloc] initWithFrame:frame];
300             topLevelPDFKitView = PDFSubview;
301         }
302         
303         ASSERT(PDFSubview);
304         
305         [topLevelPDFKitView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
306         [self addSubview:topLevelPDFKitView];
307         
308         [PDFSubview setDelegate:self];
309         written = NO;
310         // Messaging this proxy is the same as messaging PDFSubview, with the side effect that the
311         // PDF viewing defaults are updated afterwards
312         PDFSubviewProxy = (PDFView *)[[PDFPrefUpdatingProxy alloc] initWithView:self];
313     }
314     
315     return self;
316 }
317
318 - (NSMenu *)menuForEvent:(NSEvent *)theEvent
319 {
320     // Start with the menu items supplied by PDFKit, with WebKit tags applied
321     NSMutableArray *items = [self _menuItemsFromPDFKitForEvent:theEvent];
322     
323     // Add in an "Open with <default PDF viewer>" item
324     NSString *appName = nil;
325     NSImage *appIcon = nil;
326     
327     _applicationInfoForMIMEType([[dataSource response] MIMEType], &appName, &appIcon);
328     if (!appName)
329         appName = UI_STRING("Finder", "Default application name for Open With context menu");
330     
331     NSString *title = [NSString stringWithFormat:UI_STRING("Open with %@", "context menu item for PDF"), appName];
332     NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:@selector(_openWithFinder:) keyEquivalent:@""];
333     [item setTag:WebMenuItemTagOpenWithDefaultApplication];
334     if (appIcon)
335         [item setImage:appIcon];
336     [items insertObject:item atIndex:0];
337     [item release];
338     
339     [items insertObject:[NSMenuItem separatorItem] atIndex:1];
340     
341     // pass the items off to the WebKit context menu mechanism
342     WebView *webView = [[dataSource webFrame] webView];
343     ASSERT(webView);
344     NSMenu *menu = [webView _menuForElement:[self elementAtPoint:[self convertPoint:[theEvent locationInWindow] fromView:nil]] defaultItems:items];
345     
346     // The delegate has now had the opportunity to add items to the standard PDF-related items, or to
347     // remove or modify some of the PDF-related items. In 10.4, the PDF context menu did not go through 
348     // the standard WebKit delegate path, and so the standard PDF-related items always appeared. For
349     // clients that create their own context menu by hand-picking specific items from the default list, such as
350     // Safari, none of the PDF-related items will appear until the client is rewritten to explicitly
351     // include these items. For backwards compatibility of tip-of-tree WebKit with the 10.4 version of Safari
352     // (the configuration that people building open source WebKit use), we'll use the entire set of PDFKit-supplied
353     // menu items. This backward-compatibility hack won't work with any non-Safari clients, but this seems OK since
354     // (1) the symptom is fairly minor, and (2) we suspect that non-Safari clients are probably using the entire
355     // set of default items, rather than manually choosing from them. We can remove this code entirely when we
356     // ship a version of Safari that includes the fix for radar 3796579.
357     if (![self _anyPDFTagsFoundInMenu:menu] && [[[NSBundle mainBundle] bundleIdentifier] isEqualToString:@"com.apple.Safari"]) {
358         [menu addItem:[NSMenuItem separatorItem]];
359         NSEnumerator *e = [items objectEnumerator];
360         NSMenuItem *menuItem;
361         while ((menuItem = [e nextObject]) != nil) {
362             // copy menuItem since a given menuItem can be in only one menu at a time, and we don't
363             // want to mess with the menu returned from PDFKit.
364             [menu addItem:[menuItem copy]];
365         }
366     }
367     
368     return menu;
369 }
370
371 - (void)setNextKeyView:(NSView *)aView
372 {
373     // This works together with becomeFirstResponder to splice PDFSubview into
374     // the key loop similar to the way NSScrollView and NSClipView do this.
375     NSView *documentView = [PDFSubview documentView];
376     if (documentView) {
377         [documentView setNextKeyView:aView];
378         
379         // We need to make the documentView be the next view in the keyview loop.
380         // It would seem more sensible to do this in our init method, but it turns out
381         // that [NSClipView setDocumentView] won't call this method if our next key view
382         // is already set, so we wait until we're called before adding this connection.
383         // We'll also clear it when we're called with nil, so this could go through the
384         // same code path more than once successfully.
385         [super setNextKeyView: aView ? documentView : nil];
386     } else
387         [super setNextKeyView:aView];
388 }
389
390 - (void)viewDidMoveToWindow
391 {
392     // FIXME 2573089: we can observe a notification for first responder changes
393     // instead of the very frequent NSWindowDidUpdateNotification if/when 2573089 is addressed.
394     NSWindow *newWindow = [self window];
395     if (!newWindow)
396         return;
397     
398     [self _trackFirstResponder];
399     NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
400     [notificationCenter addObserver:self
401                            selector:@selector(_trackFirstResponder) 
402                                name:NSWindowDidUpdateNotification
403                              object:newWindow];
404     
405     if (previewView)
406         [notificationCenter addObserver:self
407                                selector:@selector(_receivedPDFKitLaunchNotification:)
408                                    name:PDFKitLaunchNotification
409                                  object:previewView];
410 }
411
412 - (void)viewWillMoveToWindow:(NSWindow *)window
413 {
414     // FIXME 2573089: we can observe a notification for changes to the first responder
415     // instead of the very frequent NSWindowDidUpdateNotification if/when 2573089 is addressed.
416     NSWindow *oldWindow = [self window];
417     if (!oldWindow)
418         return;
419     
420     NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
421     [notificationCenter removeObserver:self
422                                   name:NSWindowDidUpdateNotification
423                                 object:oldWindow];
424     if (previewView)
425         [notificationCenter removeObserver:self
426                                       name:PDFKitLaunchNotification
427                                     object:previewView];
428     
429     [trackedFirstResponder release];
430     trackedFirstResponder = nil;
431 }
432
433 #pragma mark NSUserInterfaceValidations PROTOCOL IMPLEMENTATION
434
435 - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)item 
436 {
437     SEL action = [item action];    
438     if (action == @selector(takeFindStringFromSelection:) || action == @selector(centerSelectionInVisibleArea:) || action == @selector(jumpToSelection:))
439         return [PDFSubview currentSelection] != nil;
440
441     return YES;
442 }
443
444 #pragma mark INTERFACE BUILDER ACTIONS FOR SAFARI
445
446 // Surprisingly enough, this isn't defined in any superclass, though it is defined in assorted AppKit classes since
447 // it's a standard menu item IBAction.
448 - (IBAction)copy:(id)sender
449 {
450     [PDFSubview copy:sender];
451 }
452
453 // This used to be a standard IBAction (for Use Selection For Find), but AppKit now uses performFindPanelAction:
454 // with a menu item tag for this purpose.
455 - (IBAction)takeFindStringFromSelection:(id)sender
456 {
457     [NSPasteboard _web_setFindPasteboardString:[[PDFSubview currentSelection] string] withOwner:self];
458 }
459
460 #pragma mark WebFrameView UNDECLARED "DELEGATE METHODS"
461
462 // This is tested in -[WebFrameView canPrintHeadersAndFooters], but isn't declared anywhere (yuck)
463 - (BOOL)canPrintHeadersAndFooters
464 {
465     return NO;
466 }
467
468 // This is tested in -[WebFrameView printOperationWithPrintInfo:], but isn't declared anywhere (yuck)
469 - (NSPrintOperation *)printOperationWithPrintInfo:(NSPrintInfo *)printInfo
470 {
471     return [[PDFSubview document] getPrintOperationForPrintInfo:printInfo autoRotate:YES];
472 }
473
474 #pragma mark WebDocumentView PROTOCOL IMPLEMENTATION
475
476 - (void)setDataSource:(WebDataSource *)ds
477 {
478     dataSource = ds;
479     [self setFrame:[[self superview] frame]];
480 }
481
482 - (void)dataSourceUpdated:(WebDataSource *)dataSource
483 {
484 }
485
486 - (void)setNeedsLayout:(BOOL)flag
487 {
488 }
489
490 - (void)layout
491 {
492 }
493
494 - (void)viewWillMoveToHostWindow:(NSWindow *)hostWindow
495 {
496 }
497
498 - (void)viewDidMoveToHostWindow
499 {
500 }
501
502 #pragma mark WebDocumentElement PROTOCOL IMPLEMENTATION
503
504 - (NSDictionary *)elementAtPoint:(NSPoint)point
505 {
506     WebFrame *frame = [dataSource webFrame];
507     ASSERT(frame);
508     
509     return [NSDictionary dictionaryWithObjectsAndKeys:
510         frame, WebElementFrameKey, 
511         [NSNumber numberWithBool:[self _pointIsInSelection:point]], WebElementIsSelectedKey,
512         nil];
513 }
514
515 - (NSDictionary *)elementAtPoint:(NSPoint)point allowShadowContent:(BOOL)allow
516 {
517     return [self elementAtPoint:point];
518 }
519
520 #pragma mark WebDocumentSearching PROTOCOL IMPLEMENTATION
521
522 - (BOOL)searchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag
523 {
524     if (![string length])
525         return NO;
526
527     // Our search algorithm, used in WebCore also, is to search in the selection first. If the found text is the
528     // entire selection, then we search again from just past the selection.
529
530     int options = 0;
531     if (!forward)
532         options |= NSBackwardsSearch;
533
534     if (!caseFlag)
535         options |= NSCaseInsensitiveSearch;
536
537     PDFDocument *document = [PDFSubview document];
538     PDFSelection *oldSelection = [PDFSubview currentSelection];
539     
540     // Initially we want to include the selected text in the search. PDFDocument's API always searches from just
541     // past the passed-in selection, so we need to pass a selection that's modified appropriately. 
542     // FIXME 4182863: Ideally we'd use a zero-length selection at the edge of the current selection, but zero-length
543     // selections don't work in PDFDocument. So instead we make a one-length selection just before or after the
544     // current selection, which works for our purposes even when the current selection is at an edge of the
545     // document.
546     PDFSelection *selectionForInitialSearch = [oldSelection copy];
547     int oldSelectionLength = [[oldSelection string] length];
548     if (forward) {
549         [selectionForInitialSearch extendSelectionAtStart:1];
550         [selectionForInitialSearch extendSelectionAtEnd:-oldSelectionLength];
551     } else {
552         [selectionForInitialSearch extendSelectionAtEnd:1];
553         [selectionForInitialSearch extendSelectionAtStart:-oldSelectionLength];
554     }
555     PDFSelection *foundSelection = [document findString:string fromSelection:selectionForInitialSearch withOptions:options];
556     [selectionForInitialSearch release];
557     
558     // If we found the selection, search again from just past the selection
559     if (_PDFSelectionsAreEqual(foundSelection, oldSelection))
560         foundSelection = [document findString:string fromSelection:oldSelection withOptions:options];
561     
562     if (!foundSelection && wrapFlag)
563         foundSelection = [document findString:string fromSelection:nil withOptions:options];
564
565     if (foundSelection) {
566         [PDFSubview setCurrentSelection:foundSelection];
567         [PDFSubview scrollSelectionToVisible:nil];
568         return YES;
569     }
570     return NO;
571 }
572
573 #pragma mark WebDocumentText PROTOCOL IMPLEMENTATION
574
575 - (BOOL)supportsTextEncoding
576 {
577     return NO;
578 }
579
580 - (NSString *)string
581 {
582     return [[PDFSubview document] string];
583 }
584
585 - (NSAttributedString *)attributedString
586 {
587     // changing the selection is a hack, but the only way to get an attr string is via PDFSelection
588     
589     // must copy this selection object because we change the selection which seems to release it
590     PDFSelection *savedSelection = [[PDFSubview currentSelection] copy];
591     [PDFSubview selectAll:nil];
592     NSAttributedString *result = [[PDFSubview currentSelection] attributedString];
593     if (savedSelection) {
594         [PDFSubview setCurrentSelection:savedSelection];
595         [savedSelection release];
596     } else {
597         // FIXME: behavior of setCurrentSelection:nil is not documented - check 4182934 for progress
598         // Otherwise, we could collapse this code with the case above.
599         [PDFSubview clearSelection];
600     }
601     
602     result = [self _scaledAttributedString:result];
603     
604     return result;
605 }
606
607 - (NSString *)selectedString
608 {
609     return [[PDFSubview currentSelection] string];
610 }
611
612 - (NSAttributedString *)selectedAttributedString
613 {
614     return [self _scaledAttributedString:[[PDFSubview currentSelection] attributedString]];
615 }
616
617 - (void)selectAll
618 {
619     [PDFSubview selectAll:nil];
620 }
621
622 - (void)deselectAll
623 {
624     [PDFSubview clearSelection];
625 }
626
627 #pragma mark WebDocumentViewState PROTOCOL IMPLEMENTATION
628
629 // Even though to WebKit we are the "docView", in reality a PDFView contains its own scrollview and docView.
630 // And it even turns out there is another PDFKit view between the docView and its enclosing ScrollView, so
631 // we have to be sure to do our calculations based on that view, immediately inside the ClipView.  We try
632 // to make as few assumptions about the PDFKit view hierarchy as possible.
633
634 - (NSPoint)scrollPoint
635 {
636     NSView *realDocView = [PDFSubview documentView];
637     NSClipView *clipView = [[realDocView enclosingScrollView] contentView];
638     return [clipView bounds].origin;
639 }
640
641 - (void)setScrollPoint:(NSPoint)p
642 {
643     WebFrame *frame = [dataSource webFrame];
644     //FIXME:  We only restore scroll state in the non-frames case because otherwise we get a crash due to
645     // PDFKit calling display from within its drawRect:. See bugzilla 4164.
646     if (![frame parentFrame]) {
647         NSView *realDocView = [PDFSubview documentView];
648         [[[realDocView enclosingScrollView] documentView] scrollPoint:p];
649     }
650 }
651
652 - (id)viewState
653 {
654     NSMutableArray *state = [NSMutableArray arrayWithCapacity:4];
655     PDFDisplayMode mode = [PDFSubview displayMode];
656     [state addObject:[NSNumber numberWithInt:mode]];
657     if (mode == kPDFDisplaySinglePage || mode == kPDFDisplayTwoUp) {
658         unsigned int pageIndex = [[PDFSubview document] indexForPage:[PDFSubview currentPage]];
659         [state addObject:[NSNumber numberWithUnsignedInt:pageIndex]];
660     }  // else in continuous modes, scroll position gets us to the right page
661     BOOL autoScaleFlag = [PDFSubview autoScales];
662     [state addObject:[NSNumber numberWithBool:autoScaleFlag]];
663     if (!autoScaleFlag)
664         [state addObject:[NSNumber numberWithFloat:[PDFSubview scaleFactor]]];
665
666     return state;
667 }
668
669 - (void)setViewState:(id)statePList
670 {
671     ASSERT([statePList isKindOfClass:[NSArray class]]);
672     NSArray *state = statePList;
673     int i = 0;
674     PDFDisplayMode mode = [[state objectAtIndex:i++] intValue];
675     [PDFSubview setDisplayMode:mode];
676     if (mode == kPDFDisplaySinglePage || mode == kPDFDisplayTwoUp) {
677         unsigned int pageIndex = [[state objectAtIndex:i++] unsignedIntValue];
678         [PDFSubview goToPage:[[PDFSubview document] pageAtIndex:pageIndex]];
679     }  // else in continuous modes, scroll position gets us to the right page
680     BOOL autoScaleFlag = [[state objectAtIndex:i++] boolValue];
681     [PDFSubview setAutoScales:autoScaleFlag];
682     if (!autoScaleFlag)
683         [PDFSubview setScaleFactor:[[state objectAtIndex:i++] floatValue]];
684 }
685
686 #pragma mark _WebDocumentTextSizing PROTOCOL IMPLEMENTATION
687
688 - (IBAction)_makeTextSmaller:(id)sender
689 {
690     [PDFSubviewProxy zoomOut:sender];
691 }
692
693 - (IBAction)_makeTextLarger:(id)sender
694 {
695     [PDFSubviewProxy zoomIn:sender];
696 }
697
698 - (IBAction)_makeTextStandardSize:(id)sender
699 {
700     [PDFSubviewProxy setScaleFactor:1.0f];
701 }
702
703 // never sent because we do not track the common size factor
704 - (void)_textSizeMultiplierChanged      { ASSERT_NOT_REACHED(); }
705
706 - (BOOL)_tracksCommonSizeFactor
707 {
708     // We keep our own scale factor instead of tracking the common one in the WebView for a couple reasons.
709     // First, PDFs tend to have visually smaller text because they are laid out for a printed page instead of
710     // the screen.  Second, the PDFView feature of AutoScaling means our scaling factor can be quiet variable.
711     return NO;
712 }
713
714 - (BOOL)_canMakeTextSmaller
715 {
716     return [PDFSubview canZoomOut];
717 }
718
719 - (BOOL)_canMakeTextLarger
720 {
721     return [PDFSubview canZoomIn];
722 }
723
724 - (BOOL)_canMakeTextStandardSize
725 {
726     return [PDFSubview scaleFactor] != 1.0;
727 }
728
729 #pragma mark WebDocumentSelection PROTOCOL IMPLEMENTATION
730
731 - (NSRect)selectionRect
732 {
733     NSRect result = NSZeroRect;
734     PDFSelection *selection = [PDFSubview currentSelection];
735     NSEnumerator *pages = [[selection pages] objectEnumerator];
736     PDFPage *page;
737     while ((page = [pages nextObject]) != nil) {
738         NSRect selectionOnPageInPDFViewCoordinates = [PDFSubview convertRect:[selection boundsForPage:page] fromPage:page];
739         if (NSIsEmptyRect(result))
740             result = selectionOnPageInPDFViewCoordinates;
741         else
742             result = NSUnionRect(result, selectionOnPageInPDFViewCoordinates);
743     }
744     
745     // Convert result to be in documentView (selectionView) coordinates
746     result = [PDFSubview convertRect:result toView:[PDFSubview documentView]];
747     
748     return result;
749 }
750
751 - (NSView *)selectionView
752 {
753     return [PDFSubview documentView];
754 }
755
756 - (NSImage *)selectionImageForcingWhiteText:(BOOL)forceWhiteText
757 {
758     // Convert the selection to an attributed string, and draw that.
759     // FIXME 4621154: this doesn't handle italics (and maybe other styles)
760     // FIXME 4604366: this doesn't handle text at non-actual size
761     NSMutableAttributedString *attributedString = [[self selectedAttributedString] mutableCopy];
762     NSRange wholeStringRange = NSMakeRange(0, [attributedString length]);
763     
764     // Modify the styles in the attributed string to draw white text, no background, and no underline. We draw 
765     // no underline because it would look ugly.
766     [attributedString beginEditing];
767     [attributedString removeAttribute:NSBackgroundColorAttributeName range:wholeStringRange];
768     [attributedString removeAttribute:NSUnderlineStyleAttributeName range:wholeStringRange];
769     if (forceWhiteText)
770         [attributedString addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithDeviceWhite:1.0f alpha:1.0f] range:wholeStringRange];
771     [attributedString endEditing];
772     
773     NSImage* selectionImage = [[[NSImage alloc] initWithSize:[self selectionImageRect].size] autorelease];
774     
775     [selectionImage lockFocus];
776     [attributedString drawAtPoint:NSZeroPoint];
777     [selectionImage unlockFocus];
778     
779     [attributedString release];
780
781     return selectionImage;
782 }
783
784 - (NSRect)selectionImageRect
785 {
786     // FIXME: deal with clipping?
787     return [self selectionRect];
788 }
789
790 - (NSArray *)pasteboardTypesForSelection
791 {
792     return [NSArray arrayWithObjects:NSRTFDPboardType, NSRTFPboardType, NSStringPboardType, nil];
793 }
794
795 - (void)writeSelectionWithPasteboardTypes:(NSArray *)types toPasteboard:(NSPasteboard *)pasteboard
796 {
797     NSAttributedString *attributedString = [self selectedAttributedString];
798     
799     if ([types containsObject:NSRTFDPboardType]) {
800         NSData *RTFDData = [attributedString RTFDFromRange:NSMakeRange(0, [attributedString length]) documentAttributes:nil];
801         [pasteboard setData:RTFDData forType:NSRTFDPboardType];
802     }        
803     
804     if ([types containsObject:NSRTFPboardType]) {
805         if ([attributedString containsAttachments])
806             attributedString = [attributedString _web_attributedStringByStrippingAttachmentCharacters];
807
808         NSData *RTFData = [attributedString RTFFromRange:NSMakeRange(0, [attributedString length]) documentAttributes:nil];
809         [pasteboard setData:RTFData forType:NSRTFPboardType];
810     }
811     
812     if ([types containsObject:NSStringPboardType])
813         [pasteboard setString:[self selectedString] forType:NSStringPboardType];
814 }
815
816 #pragma mark PDFView DELEGATE METHODS
817
818 // Delegates implementing the following method will be called to handle clicks on URL
819 // links within the PDFView.  
820 - (void)PDFViewWillClickOnLink:(PDFView *)sender withURL:(NSURL *)URL
821 {
822     if (URL)
823         [[dataSource webFrame] _frameLoader]->safeLoad(URL);
824 }
825
826 @end
827
828 @implementation WebPDFView (FileInternal)
829
830 + (Class)_PDFPreviewViewClass
831 {
832     static Class PDFPreviewViewClass = nil;
833     static BOOL checkedForPDFPreviewViewClass = NO;
834     
835     if (!checkedForPDFPreviewViewClass) {
836         checkedForPDFPreviewViewClass = YES;
837         PDFPreviewViewClass = [[WebPDFView PDFKitBundle] classNamed:@"PDFPreviewView"];
838     }
839     
840     // This class might not be available; callers need to deal with a nil return here.
841     return PDFPreviewViewClass;
842 }
843
844 + (Class)_PDFViewClass
845 {
846     static Class PDFViewClass = nil;
847     if (PDFViewClass == nil) {
848         PDFViewClass = [[WebPDFView PDFKitBundle] classNamed:@"PDFView"];
849         if (!PDFViewClass)
850             LOG_ERROR("Couldn't find PDFView class in PDFKit.framework");
851     }
852     return PDFViewClass;
853 }
854
855 - (BOOL)_anyPDFTagsFoundInMenu:(NSMenu *)menu
856 {
857     NSEnumerator *e = [[menu itemArray] objectEnumerator];
858     NSMenuItem *item;
859     while ((item = [e nextObject]) != nil) {
860         switch ([item tag]) {
861             case WebMenuItemTagOpenWithDefaultApplication:
862             case WebMenuItemPDFActualSize:
863             case WebMenuItemPDFZoomIn:
864             case WebMenuItemPDFZoomOut:
865             case WebMenuItemPDFAutoSize:
866             case WebMenuItemPDFSinglePage:
867             case WebMenuItemPDFFacingPages:
868             case WebMenuItemPDFContinuous:
869             case WebMenuItemPDFNextPage:
870             case WebMenuItemPDFPreviousPage:
871                 return YES;
872         }
873     }
874     return NO;
875 }
876
877 - (void)_applyPDFDefaults
878 {
879     // Set up default viewing params
880     WebPreferences *prefs = [[dataSource _webView] preferences];
881     float scaleFactor = [prefs PDFScaleFactor];
882     if (scaleFactor == 0)
883         [PDFSubview setAutoScales:YES];
884     else {
885         [PDFSubview setAutoScales:NO];
886         [PDFSubview setScaleFactor:scaleFactor];
887     }
888     [PDFSubview setDisplayMode:[prefs PDFDisplayMode]];    
889 }
890
891 - (NSEvent *)_fakeKeyEventWithFunctionKey:(unichar)functionKey
892 {
893     // FIXME 4400480: when PDFView implements the standard scrolling selectors that this
894     // method is used to mimic, we can eliminate this method and call them directly.
895     NSString *keyAsString = [NSString stringWithCharacters:&functionKey length:1];
896     return [NSEvent keyEventWithType:NSKeyDown
897                             location:NSZeroPoint
898                        modifierFlags:0
899                            timestamp:0
900                         windowNumber:0
901                              context:nil
902                           characters:keyAsString
903          charactersIgnoringModifiers:keyAsString
904                            isARepeat:NO
905                              keyCode:0];
906 }
907
908 - (NSMutableArray *)_menuItemsFromPDFKitForEvent:(NSEvent *)theEvent
909 {
910     NSMutableArray *copiedItems = [NSMutableArray array];
911     NSDictionary *actionsToTags = [[NSDictionary alloc] initWithObjectsAndKeys:
912         [NSNumber numberWithInt:WebMenuItemPDFActualSize], NSStringFromSelector(@selector(_setActualSize:)),
913         [NSNumber numberWithInt:WebMenuItemPDFZoomIn], NSStringFromSelector(@selector(zoomIn:)),
914         [NSNumber numberWithInt:WebMenuItemPDFZoomOut], NSStringFromSelector(@selector(zoomOut:)),
915         [NSNumber numberWithInt:WebMenuItemPDFAutoSize], NSStringFromSelector(@selector(_setAutoSize:)),
916         [NSNumber numberWithInt:WebMenuItemPDFSinglePage], NSStringFromSelector(@selector(_setSinglePage:)),
917         [NSNumber numberWithInt:WebMenuItemPDFFacingPages], NSStringFromSelector(@selector(_setDoublePage:)),
918         [NSNumber numberWithInt:WebMenuItemPDFContinuous], NSStringFromSelector(@selector(_toggleContinuous:)),
919         [NSNumber numberWithInt:WebMenuItemPDFNextPage], NSStringFromSelector(@selector(goToNextPage:)),
920         [NSNumber numberWithInt:WebMenuItemPDFPreviousPage], NSStringFromSelector(@selector(goToPreviousPage:)),
921         nil];
922     
923     NSEnumerator *e = [[[PDFSubview menuForEvent:theEvent] itemArray] objectEnumerator];
924     NSMenuItem *item;
925     while ((item = [e nextObject]) != nil) {
926         // Copy items since a menu item can be in only one menu at a time, and we don't
927         // want to modify the original menu supplied by PDFKit.
928         NSMenuItem *itemCopy = [item copy];
929         [copiedItems addObject:itemCopy];
930         
931         if ([itemCopy isSeparatorItem])
932             continue;
933
934         NSString *actionString = NSStringFromSelector([itemCopy action]);
935         NSNumber *tagNumber = [actionsToTags objectForKey:actionString];
936         
937         int tag;
938         if (tagNumber != nil)
939             tag = [tagNumber intValue];
940         else {
941             tag = WebMenuItemTagOther;
942             LOG_ERROR("no WebKit menu item tag found for PDF context menu item action \"%@\", using WebMenuItemTagOther", actionString);
943         }
944         
945         if ([itemCopy tag] == 0) {
946             [itemCopy setTag:tag];
947             if ([itemCopy target] == PDFSubview) {
948                 // Note that updating the defaults is cheap because it catches redundant settings, so installing
949                 // the proxy for actions that don't impact the defaults is OK
950                 [itemCopy setTarget:PDFSubviewProxy];
951             }
952         } else
953             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]);
954     }
955     
956     [actionsToTags release];
957     
958     return copiedItems;
959 }
960
961 - (void)_openWithFinder:(id)sender
962 {
963     NSString *opath = [self _path];
964     
965     if (opath) {
966         if (!written) {
967             [[dataSource data] writeToFile:opath atomically:YES];
968             written = YES;
969         }
970         
971         if (![[NSWorkspace sharedWorkspace] openFile:opath]) {
972             // NSWorkspace couldn't open file.  Do we need an alert
973             // here?  We ignore the error elsewhere.
974         }
975     }
976 }
977
978 - (NSString *)_path
979 {
980     // Generate path once.
981     if (path)
982         return path;
983     
984     NSString *filename = [[dataSource response] suggestedFilename];
985     NSFileManager *manager = [NSFileManager defaultManager];    
986     
987     path = [@"/tmp/" stringByAppendingPathComponent: filename];
988     if ([manager fileExistsAtPath:path]) {
989         path = [OBJC_TEMP_PREFIX stringByAppendingString:filename];
990         char *cpath = (char *)[path fileSystemRepresentation];
991         
992         int fd = mkstemps (cpath, strlen(cpath) - strlen(TEMP_PREFIX) + 1);
993         if (fd < 0) {
994             // Couldn't create a temporary file!  Should never happen.  Do
995             // we need an alert here?
996             path = nil;
997         } else {
998             close (fd);
999             path = [manager stringWithFileSystemRepresentation:cpath length:strlen(cpath)];
1000         }
1001     }
1002     
1003     [path retain];
1004     
1005     return path;
1006 }
1007
1008 - (PDFView *)_PDFSubview
1009 {
1010     return PDFSubview;
1011 }
1012
1013 - (BOOL)_pointIsInSelection:(NSPoint)point
1014 {
1015     PDFPage *page = [PDFSubview pageForPoint:point nearest:NO];
1016     if (!page)
1017         return NO;
1018     
1019     NSRect selectionRect = [PDFSubview convertRect:[[PDFSubview currentSelection] boundsForPage:page] fromPage:page];
1020     
1021     return NSPointInRect(point, selectionRect);
1022 }
1023
1024 - (void)_receivedPDFKitLaunchNotification:(NSNotification *)notification
1025 {
1026     ASSERT([notification object] == previewView);
1027     [self _openWithFinder:self];
1028 }
1029
1030 - (NSAttributedString *)_scaledAttributedString:(NSAttributedString *)unscaledAttributedString
1031 {
1032     NSLog(@"unscaledAttributedString is %@", unscaledAttributedString);
1033     if (!unscaledAttributedString)
1034         return nil;
1035     
1036     float scaleFactor = [PDFSubview scaleFactor];
1037     if (scaleFactor == 1.0)
1038         return unscaledAttributedString;
1039     
1040     NSMutableAttributedString *result = [[unscaledAttributedString mutableCopy] autorelease];
1041     unsigned int length = [result length];
1042     NSRange effectiveRange = NSMakeRange(0,0);
1043     
1044     [result beginEditing];    
1045     while (NSMaxRange(effectiveRange) < length) {
1046         NSFont *unscaledFont = [result attribute:NSFontAttributeName atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange];
1047         NSFont *scaledFont = [NSFont fontWithName:[unscaledFont fontName] size:[unscaledFont pointSize]*scaleFactor];
1048         [result addAttribute:NSFontAttributeName value:scaledFont range:effectiveRange];
1049     }
1050     [result endEditing];
1051     
1052     return result;
1053 }
1054
1055 - (void)_trackFirstResponder
1056 {
1057     ASSERT([self window]);
1058     
1059     id newFirstResponder = [[self window] firstResponder];
1060     if (newFirstResponder == trackedFirstResponder)
1061         return;
1062     
1063     // This next clause is the entire purpose of _trackFirstResponder. In other WebDocument
1064     // view classes this is done in a resignFirstResponder override, but in this case the
1065     // first responder view is a PDFKit class that we can't subclass.
1066     if (trackedFirstResponder == [PDFSubview documentView] && ![[dataSource _webView] maintainsInactiveSelection])
1067         [self deselectAll];
1068     
1069     
1070     [trackedFirstResponder release];
1071     trackedFirstResponder = [newFirstResponder retain];
1072 }
1073
1074 @end;
1075
1076 @implementation PDFPrefUpdatingProxy
1077
1078 - (id)initWithView:(WebPDFView *)aView
1079 {
1080     // No [super init], since we inherit from NSProxy
1081     view = aView;
1082     return self;
1083 }
1084
1085 - (void)forwardInvocation:(NSInvocation *)invocation
1086 {
1087     PDFView *PDFSubview = [view _PDFSubview];
1088     [invocation invokeWithTarget:PDFSubview];
1089
1090     WebPreferences *prefs = [[view->dataSource _webView] preferences];
1091     float scaleFactor = [PDFSubview autoScales] ? 0.0f : [PDFSubview scaleFactor];
1092     [prefs setPDFScaleFactor:scaleFactor];
1093     [prefs setPDFDisplayMode:[PDFSubview displayMode]];
1094 }
1095
1096 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
1097 {
1098     return [[view _PDFSubview] methodSignatureForSelector:sel];
1099 }
1100
1101 @end