Reviewed by Chris Blumenberg.
[WebKit-https.git] / WebKit / WebView.subproj / WebPDFView.m
1 /*
2  * Copyright (C) 2005 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 #ifndef OMIT_TIGER_FEATURES
30
31 #import <WebKit/WebAssertions.h>
32 #import <WebKit/WebDataSource.h>
33 #import <WebKit/WebDocumentInternal.h>
34 #import <WebKit/WebFrame.h>
35 #import <WebKit/WebLocalizableStrings.h>
36 #import <WebKit/WebNSPasteboardExtras.h>
37 #import <WebKit/WebPDFView.h>
38 #import <WebKit/WebUIDelegate.h>
39 #import <WebKit/WebView.h>
40 #import <WebKit/WebViewPrivate.h>
41
42 #import <Quartz/Quartz.h>
43
44 // QuartzPrivate.h doesn't include the PDFKit private headers, so we can't get at PDFViewPriv.h. (3957971)
45 // Even if that was fixed, we'd have to tweak compile options to include QuartzPrivate.h. (3957839)
46
47 @interface PDFDocument (PDFKitSecretsIKnow)
48 - (NSPrintOperation *)getPrintOperationForPrintInfo:(NSPrintInfo *)printInfo autoRotate:(BOOL)doRotate;
49 @end
50
51 NSString *_NSPathForSystemFramework(NSString *framework);
52
53 @implementation WebPDFView
54
55 + (NSBundle *)PDFKitBundle
56 {
57     static NSBundle *PDFKitBundle = nil;
58     if (PDFKitBundle == nil) {
59         NSString *PDFKitPath = [_NSPathForSystemFramework(@"Quartz.framework") stringByAppendingString:@"/Frameworks/PDFKit.framework"];
60         if (PDFKitPath == nil) {
61             ERROR("Couldn't find PDFKit.framework");
62             return nil;
63         }
64         PDFKitBundle = [NSBundle bundleWithPath:PDFKitPath];
65         if (![PDFKitBundle load]) {
66             ERROR("Couldn't load PDFKit.framework");
67         }
68     }
69     return PDFKitBundle;
70 }
71
72 + (Class)PDFViewClass
73 {
74     static Class PDFViewClass = nil;
75     if (PDFViewClass == nil) {
76         PDFViewClass = [[WebPDFView PDFKitBundle] classNamed:@"PDFView"];
77         if (PDFViewClass == nil) {
78             ERROR("Couldn't find PDFView class in PDFKit.framework");
79         }
80     }
81     return PDFViewClass;
82 }
83
84 - (id)initWithFrame:(NSRect)frame
85 {
86     self = [super initWithFrame:frame];
87     if (self) {
88         [self setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
89         PDFSubview = [[[[self class] PDFViewClass] alloc] initWithFrame:frame];
90         [PDFSubview setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
91         [self addSubview:PDFSubview];
92         written = NO;
93     }
94     return self;
95 }
96
97 - (void)dealloc
98 {
99     [PDFSubview release];
100     [path release];
101     [super dealloc];
102 }
103
104 - (PDFView *)PDFSubview
105 {
106     return PDFSubview;
107 }
108
109 #define TEMP_PREFIX "/tmp/XXXXXX-"
110 #define OBJC_TEMP_PREFIX @"/tmp/XXXXXX-"
111
112 static void applicationInfoForMIMEType(NSString *type, NSString **name, NSImage **image)
113 {
114     NSURL *appURL = nil;
115     
116     OSStatus error = LSCopyApplicationForMIMEType((CFStringRef)type, kLSRolesAll, (CFURLRef *)&appURL);
117     if(error != noErr){
118         return;
119     }
120
121     NSString *appPath = [appURL path];
122     CFRelease (appURL);
123     
124     *image = [[NSWorkspace sharedWorkspace] iconForFile:appPath];  
125     [*image setSize:NSMakeSize(16.f,16.f)];  
126     
127     NSString *appName = [[NSFileManager defaultManager] displayNameAtPath:appPath];
128     *name = appName;
129 }
130
131 - (NSString *)path
132 {
133     // Generate path once.
134     if (path)
135         return path;
136         
137     NSString *filename = [[dataSource response] suggestedFilename];
138     NSFileManager *manager = [NSFileManager defaultManager];    
139     
140     path = [@"/tmp/" stringByAppendingPathComponent: filename];
141     if ([manager fileExistsAtPath:path]) {
142         path = [OBJC_TEMP_PREFIX stringByAppendingString:filename];
143         char *cpath = (char *)[path fileSystemRepresentation];
144         
145         int fd = mkstemps (cpath, strlen(cpath) - strlen(TEMP_PREFIX) + 1);
146         if (fd < 0) {
147             // Couldn't create a temporary file!  Should never happen.  Do
148             // we need an alert here?
149             path = nil;
150         }
151         else {
152             close (fd);
153             path = [manager stringWithFileSystemRepresentation:cpath length:strlen(cpath)];
154         }
155     }
156     
157     [path retain];
158     
159     return path;
160 }
161
162 - (NSView *)hitTest:(NSPoint)point
163 {
164     // Override hitTest so we can override menuForEvent.
165     NSEvent *event = [NSApp currentEvent];
166     NSEventType type = [event type];
167     if (type == NSRightMouseDown || (type == NSLeftMouseDown && ([event modifierFlags] & NSControlKeyMask))) {
168         return self;
169     }
170     return [super hitTest:point];
171 }
172
173 - (NSDictionary *)elementAtPoint:(NSPoint)point
174 {
175     WebFrame *frame = [dataSource webFrame];
176     ASSERT(frame);
177
178     // FIXME 4158121: should determine whether the point is over a selection, and if so set WebElementIsSelectedKey
179     // as in WebTextView.m. Would need to convert coordinates, and make sure that the code that checks
180     // WebElementIsSelectedKey would work with PDF documents.
181     return [NSDictionary dictionaryWithObjectsAndKeys:
182         frame, WebElementFrameKey, nil];
183 }
184
185 - (NSMutableArray *)_menuItemsFromPDFKitForEvent:(NSEvent *)theEvent
186 {
187     NSMutableArray *copiedItems = [NSMutableArray array];
188     NSDictionary *actionsToTags = [[NSDictionary alloc] initWithObjectsAndKeys:
189         [NSNumber numberWithInt:WebMenuItemPDFActualSize], NSStringFromSelector(@selector(_setActualSize:)),
190         [NSNumber numberWithInt:WebMenuItemPDFZoomIn], NSStringFromSelector(@selector(zoomIn:)),
191         [NSNumber numberWithInt:WebMenuItemPDFZoomOut], NSStringFromSelector(@selector(zoomOut:)),
192         [NSNumber numberWithInt:WebMenuItemPDFAutoSize], NSStringFromSelector(@selector(_setAutoSize:)),
193         [NSNumber numberWithInt:WebMenuItemPDFSinglePage], NSStringFromSelector(@selector(_setSinglePage:)),
194         [NSNumber numberWithInt:WebMenuItemPDFFacingPages], NSStringFromSelector(@selector(_setDoublePage:)),
195         [NSNumber numberWithInt:WebMenuItemPDFContinuous], NSStringFromSelector(@selector(_toggleContinuous:)),
196         [NSNumber numberWithInt:WebMenuItemPDFNextPage], NSStringFromSelector(@selector(goToNextPage:)),
197         [NSNumber numberWithInt:WebMenuItemPDFPreviousPage], NSStringFromSelector(@selector(goToPreviousPage:)),
198         nil];
199     
200     NSEnumerator *e = [[[PDFSubview menuForEvent:theEvent] itemArray] objectEnumerator];
201     NSMenuItem *item;
202     while ((item = [e nextObject]) != nil) {
203         // Copy items since a menu item can be in only one menu at a time, and we don't
204         // want to modify the original menu supplied by PDFKit.
205         NSMenuItem *itemCopy = [item copy];
206         [copiedItems addObject:itemCopy];
207         
208         if ([itemCopy isSeparatorItem]) {
209             continue;
210         }
211         NSString *actionString = NSStringFromSelector([itemCopy action]);
212         NSNumber *tagNumber = [actionsToTags objectForKey:actionString];
213         
214         int tag;
215         if (tagNumber != nil) {
216             tag = [tagNumber intValue];
217         } else {
218             tag = WebMenuItemTagOther;
219             ERROR("no WebKit menu item tag found for PDF context menu item action \"%@\", using WebMenuItemTagOther", actionString);
220         }
221         if ([itemCopy tag] == 0) {
222             [itemCopy setTag:tag];
223         } else {
224             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]);
225         }        
226     }
227     
228     [actionsToTags release];
229     
230     return copiedItems;
231 }
232
233 - (NSMenu *)menuForEvent:(NSEvent *)theEvent
234 {
235     // Start with the menu items supplied by PDFKit, with WebKit tags applied
236     NSMutableArray *items = [self _menuItemsFromPDFKitForEvent:theEvent];
237
238     // Add in an "Open with <default PDF viewer>" item
239     NSString *appName = nil;
240     NSImage *appIcon = nil;
241     
242     applicationInfoForMIMEType([[dataSource response] MIMEType], &appName, &appIcon);
243     if (!appName)
244         appName = UI_STRING("Finder", "Default application name for Open With context menu");
245     
246     NSString *title = [NSString stringWithFormat:UI_STRING("Open with %@", "context menu item for PDF"), appName];
247     NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:@selector(openWithFinder:) keyEquivalent:@""];
248     [item setTag:WebMenuItemTagOpenWithDefaultApplication];
249     if (appIcon)
250         [item setImage:appIcon];
251     [items insertObject:item atIndex:0];
252     [item release];
253     
254     [items insertObject:[NSMenuItem separatorItem] atIndex:1];
255     
256     // pass the items off to the WebKit context menu mechanism
257     WebView *webView = [[dataSource webFrame] webView];
258     ASSERT(webView);
259     // Currently clicks anywhere in the PDF view are treated the same, so we just pass NSZeroPoint;
260     // we implement elementAtPoint: here just to be slightly forward-looking.
261     return [webView _menuForElement:[self elementAtPoint:NSZeroPoint] defaultItems:items];
262 }
263
264 - (void)_updateScalingToReflectTextSize
265 {
266     WebView *view = [[dataSource webFrame] webView];
267     
268     // The scale factor and text size multiplier conveniently use the same units, so we can just
269     // treat the values as interchangeable.
270     if (view != nil) {
271         [PDFSubview setScaleFactor:[view textSizeMultiplier]];          
272     }   
273 }
274
275 - (void)setDataSource:(WebDataSource *)ds
276 {
277     dataSource = ds;
278     [self setFrame:[[self superview] frame]];
279     [self _updateScalingToReflectTextSize];
280 }
281
282 - (void)dataSourceUpdated:(WebDataSource *)dataSource
283 {
284 }
285
286 - (void)setNeedsLayout:(BOOL)flag
287 {
288 }
289
290 - (void)layout
291 {
292 }
293
294 - (void)viewWillMoveToHostWindow:(NSWindow *)hostWindow
295 {
296 }
297
298 - (void)viewDidMoveToHostWindow
299 {
300 }
301
302 - (void)openWithFinder:(id)sender
303 {
304     NSString *opath = [self path];
305     
306     if (opath) {
307         if (!written) {
308             [[dataSource data] writeToFile:opath atomically:YES];
309             written = YES;
310         }
311     
312         if (![[NSWorkspace sharedWorkspace] openFile:opath]) {
313             // NSWorkspace couldn't open file.  Do we need an alert
314             // here?  We ignore the error elsewhere.
315         }
316     }
317 }
318
319 - (void)_web_textSizeMultiplierChanged
320 {
321     [self _updateScalingToReflectTextSize];
322 }
323
324 - (BOOL)searchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag;
325 {
326     int options = 0;
327     if (!forward) {
328         options |= NSBackwardsSearch;
329     }
330     if (!caseFlag) {
331         options |= NSCaseInsensitiveSearch;
332     }
333     PDFDocument *document = [PDFSubview document];
334     PDFSelection *selection = [document findString:string fromSelection:[PDFSubview currentSelection] withOptions:options];
335     if (selection == nil && wrapFlag) {
336         selection = [document findString:string fromSelection:nil withOptions:options];
337     }
338     if (selection != nil) {
339         [PDFSubview setCurrentSelection:selection];
340         [PDFSubview scrollSelectionToVisible:nil];
341         return YES;
342     }
343     return NO;
344 }
345
346 - (void)takeFindStringFromSelection:(id)sender
347 {
348     [NSPasteboard _web_setFindPasteboardString:[[PDFSubview currentSelection] string] withOwner:self];
349 }
350
351 - (void)jumpToSelection:(id)sender
352 {
353     [PDFSubview scrollSelectionToVisible:nil];
354 }
355
356 - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)item 
357 {
358     SEL action = [item action];    
359     if (action == @selector(takeFindStringFromSelection:) || action == @selector(jumpToSelection:)) {
360         return [PDFSubview currentSelection] != nil;
361     }
362     return YES;
363 }
364
365 - (BOOL)canPrintHeadersAndFooters
366 {
367     return NO;
368 }
369
370 - (NSPrintOperation *)printOperationWithPrintInfo:(NSPrintInfo *)printInfo
371 {
372     return [[PDFSubview document] getPrintOperationForPrintInfo:printInfo autoRotate:YES];
373 }
374
375 @end
376
377 #endif // OMIT_TIGER_FEATURES