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