1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #import <Cocoa/Cocoa.h>
8 #include "nsFilePicker.h"
10 #include "nsReadableUtils.h"
11 #include "nsNetUtil.h"
12 #include "nsIComponentManager.h"
14 #include "nsILocalFileMac.h"
16 #include "nsArrayEnumerator.h"
17 #include "nsIStringBundle.h"
18 #include "nsCocoaFeatures.h"
19 #include "nsCocoaUtils.h"
20 #include "mozilla/Preferences.h"
22 // This must be included last:
23 #include "nsObjCExceptions.h"
25 using namespace mozilla;
27 const float kAccessoryViewPadding = 5;
28 const int kSaveTypeControlTag = 1;
30 static bool gCallSecretHiddenFileAPI = false;
31 const char kShowHiddenFilesPref[] = "filepicker.showHiddenFiles";
34 * This class is an observer of NSPopUpButton selection change.
36 @interface NSPopUpButtonObserver : NSObject
38 NSPopUpButton* mPopUpButton;
39 NSOpenPanel* mOpenPanel;
40 nsFilePicker* mFilePicker;
42 - (void) setPopUpButton:(NSPopUpButton*)aPopUpButton;
43 - (void) setOpenPanel:(NSOpenPanel*)aOpenPanel;
44 - (void) setFilePicker:(nsFilePicker*)aFilePicker;
45 - (void) menuChangedItem:(NSNotification*)aSender;
48 NS_IMPL_ISUPPORTS1(nsFilePicker, nsIFilePicker)
50 // We never want to call the secret show hidden files API unless the pref
51 // has been set. Once the pref has been set we always need to call it even
52 // if it disappears so that we stop showing hidden files if a user deletes
53 // the pref. If the secret API was used once and things worked out it should
54 // continue working for subsequent calls so the user is at no more risk.
55 static void SetShowHiddenFileState(NSSavePanel* panel)
57 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
60 if (NS_SUCCEEDED(Preferences::GetBool(kShowHiddenFilesPref, &show))) {
61 gCallSecretHiddenFileAPI = true;
64 if (gCallSecretHiddenFileAPI) {
65 // invoke a method to get a Cocoa-internal nav view
66 SEL navViewSelector = @selector(_navView);
67 NSMethodSignature* navViewSignature = [panel methodSignatureForSelector:navViewSelector];
68 if (!navViewSignature)
70 NSInvocation* navViewInvocation = [NSInvocation invocationWithMethodSignature:navViewSignature];
71 [navViewInvocation setSelector:navViewSelector];
72 [navViewInvocation setTarget:panel];
73 [navViewInvocation invoke];
75 // get the returned nav view
77 [navViewInvocation getReturnValue:&navView];
79 // invoke the secret show hidden file state method on the nav view
80 SEL showHiddenFilesSelector = @selector(setShowsHiddenFiles:);
81 NSMethodSignature* showHiddenFilesSignature = [navView methodSignatureForSelector:showHiddenFilesSelector];
82 if (!showHiddenFilesSignature)
84 NSInvocation* showHiddenFilesInvocation = [NSInvocation invocationWithMethodSignature:showHiddenFilesSignature];
85 [showHiddenFilesInvocation setSelector:showHiddenFilesSelector];
86 [showHiddenFilesInvocation setTarget:navView];
87 [showHiddenFilesInvocation setArgument:&show atIndex:2];
88 [showHiddenFilesInvocation invoke];
91 NS_OBJC_END_TRY_ABORT_BLOCK;
94 nsFilePicker::nsFilePicker()
95 : mSelectedTypeIndex(0)
99 nsFilePicker::~nsFilePicker()
104 nsFilePicker::InitNative(nsIWidget *aParent, const nsAString& aTitle)
109 NSView* nsFilePicker::GetAccessoryView()
111 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
113 NSView* accessoryView = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)] autorelease];
115 // Set a label's default value.
116 NSString* label = @"Format:";
118 // Try to get the localized string.
119 nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
120 nsCOMPtr<nsIStringBundle> bundle;
121 nsresult rv = sbs->CreateBundle("chrome://global/locale/filepicker.properties", getter_AddRefs(bundle));
122 if (NS_SUCCEEDED(rv)) {
123 nsXPIDLString locaLabel;
124 bundle->GetStringFromName(NS_LITERAL_STRING("formatLabel").get(),
125 getter_Copies(locaLabel));
127 label = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(locaLabel.get())
128 length:locaLabel.Length()];
132 // set up label text field
133 NSTextField* textField = [[[NSTextField alloc] init] autorelease];
134 [textField setEditable:NO];
135 [textField setSelectable:NO];
136 [textField setDrawsBackground:NO];
137 [textField setBezeled:NO];
138 [textField setBordered:NO];
139 [textField setFont:[NSFont labelFontOfSize:13.0]];
140 [textField setStringValue:label];
141 [textField setTag:0];
142 [textField sizeToFit];
144 // set up popup button
145 NSPopUpButton* popupButton = [[[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 0, 0, 0) pullsDown:NO] autorelease];
146 uint32_t numMenuItems = mTitles.Length();
147 for (uint32_t i = 0; i < numMenuItems; i++) {
148 const nsString& currentTitle = mTitles[i];
149 NSString *titleString;
150 if (currentTitle.IsEmpty()) {
151 const nsString& currentFilter = mFilters[i];
152 titleString = [[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(currentFilter.get())
153 length:currentFilter.Length()];
156 titleString = [[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(currentTitle.get())
157 length:currentTitle.Length()];
159 [popupButton addItemWithTitle:titleString];
160 [titleString release];
162 if (mSelectedTypeIndex >= 0 && (uint32_t)mSelectedTypeIndex < numMenuItems)
163 [popupButton selectItemAtIndex:mSelectedTypeIndex];
164 [popupButton setTag:kSaveTypeControlTag];
165 [popupButton sizeToFit]; // we have to do sizeToFit to get the height calculated for us
166 // This is just a default width that works well, doesn't truncate the vast majority of
167 // things that might end up in the menu.
168 [popupButton setFrameSize:NSMakeSize(180, [popupButton frame].size.height)];
170 // position everything based on control sizes with kAccessoryViewPadding pix padding
171 // on each side kAccessoryViewPadding pix horizontal padding between controls
172 float greatestHeight = [textField frame].size.height;
173 if ([popupButton frame].size.height > greatestHeight)
174 greatestHeight = [popupButton frame].size.height;
175 float totalViewHeight = greatestHeight + kAccessoryViewPadding * 2;
176 float totalViewWidth = [textField frame].size.width + [popupButton frame].size.width + kAccessoryViewPadding * 3;
177 [accessoryView setFrameSize:NSMakeSize(totalViewWidth, totalViewHeight)];
179 float textFieldOriginY = ((greatestHeight - [textField frame].size.height) / 2 + 1) + kAccessoryViewPadding;
180 [textField setFrameOrigin:NSMakePoint(kAccessoryViewPadding, textFieldOriginY)];
182 float popupOriginX = [textField frame].size.width + kAccessoryViewPadding * 2;
183 float popupOriginY = ((greatestHeight - [popupButton frame].size.height) / 2) + kAccessoryViewPadding;
184 [popupButton setFrameOrigin:NSMakePoint(popupOriginX, popupOriginY)];
186 [accessoryView addSubview:textField];
187 [accessoryView addSubview:popupButton];
188 return accessoryView;
190 NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
193 // Display the file dialog
194 NS_IMETHODIMP nsFilePicker::Show(int16_t *retval)
196 NS_ENSURE_ARG_POINTER(retval);
198 *retval = returnCancel;
200 int16_t userClicksOK = returnCancel;
202 // Random questions from DHH:
204 // Why do we pass mTitle, mDefault to the functions? Can GetLocalFile. PutLocalFile,
205 // and GetLocalFolder get called someplace else? It generates a bunch of warnings
206 // as it is right now.
208 // I think we could easily combine GetLocalFile and GetLocalFolder together, just
209 // setting panel pick options based on mMode. I didn't do it here b/c I wanted to
210 // make this look as much like Carbon nsFilePicker as possible.
213 nsCOMPtr<nsIFile> theFile;
218 userClicksOK = GetLocalFiles(mTitle, false, mFiles);
221 case modeOpenMultiple:
222 userClicksOK = GetLocalFiles(mTitle, true, mFiles);
226 userClicksOK = PutLocalFile(mTitle, mDefault, getter_AddRefs(theFile));
230 userClicksOK = GetLocalFolder(mTitle, getter_AddRefs(theFile));
234 NS_ERROR("Unknown file picker mode");
239 mFiles.AppendObject(theFile);
241 *retval = userClicksOK;
246 void UpdatePanelFileTypes(NSOpenPanel* aPanel, NSArray* aFilters)
248 // If we show all file types, also "expose" bundles' contents.
249 [aPanel setTreatsFilePackagesAsDirectories:!aFilters];
251 [aPanel setAllowedFileTypes:aFilters];
254 @implementation NSPopUpButtonObserver
255 - (void) setPopUpButton:(NSPopUpButton*)aPopUpButton
257 mPopUpButton = aPopUpButton;
260 - (void) setOpenPanel:(NSOpenPanel*)aOpenPanel
262 mOpenPanel = aOpenPanel;
265 - (void) setFilePicker:(nsFilePicker*)aFilePicker
267 mFilePicker = aFilePicker;
270 - (void) menuChangedItem:(NSNotification *)aSender
272 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
273 int32_t selectedItem = [mPopUpButton indexOfSelectedItem];
274 if (selectedItem < 0) {
278 mFilePicker->SetFilterIndex(selectedItem);
279 UpdatePanelFileTypes(mOpenPanel, mFilePicker->GetFilterList());
281 NS_OBJC_END_TRY_ABORT_BLOCK_RETURN();
285 // Use OpenPanel to do a GetFile. Returns |returnOK| if the user presses OK in the dialog.
287 nsFilePicker::GetLocalFiles(const nsString& inTitle, bool inAllowMultiple, nsCOMArray<nsIFile>& outFiles)
289 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
291 int16_t retVal = (int16_t)returnCancel;
292 NSOpenPanel *thePanel = [NSOpenPanel openPanel];
294 SetShowHiddenFileState(thePanel);
296 // Set the options for how the get file dialog will appear
297 SetDialogTitle(inTitle, thePanel);
298 [thePanel setAllowsMultipleSelection:inAllowMultiple];
299 [thePanel setCanSelectHiddenExtension:YES];
300 [thePanel setCanChooseDirectories:NO];
301 [thePanel setCanChooseFiles:YES];
302 [thePanel setResolvesAliases:YES]; //this is default - probably doesn't need to be set
305 // filters may be null, if we should allow all file types.
306 NSArray *filters = GetFilterList();
308 // set up default directory
309 NSString *theDir = PanelDefaultDirectory();
311 // if this is the "Choose application..." dialog, and no other start
312 // dir has been set, then use the Applications folder.
314 if (filters && [filters count] == 1 &&
315 [(NSString *)[filters objectAtIndex:0] isEqualToString:@"app"])
316 theDir = @"/Applications/";
322 [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
326 nsCocoaUtils::PrepareForNativeAppModalDialog();
327 if (mFilters.Length() > 1) {
328 // [NSURL initWithString:] (below) throws an exception if URLString is nil.
330 NSPopUpButtonObserver* observer = [[NSPopUpButtonObserver alloc] init];
332 NSView* accessoryView = GetAccessoryView();
333 [thePanel setAccessoryView:accessoryView];
335 [observer setPopUpButton:[accessoryView viewWithTag:kSaveTypeControlTag]];
336 [observer setOpenPanel:thePanel];
337 [observer setFilePicker:this];
339 [[NSNotificationCenter defaultCenter]
341 selector:@selector(menuChangedItem:)
342 name:NSMenuWillSendActionNotification object:nil];
344 UpdatePanelFileTypes(thePanel, filters);
345 result = [thePanel runModal];
347 [[NSNotificationCenter defaultCenter] removeObserver:observer];
350 // If we show all file types, also "expose" bundles' contents.
352 [thePanel setTreatsFilePackagesAsDirectories:YES];
354 [thePanel setAllowedFileTypes:filters];
355 result = [thePanel runModal];
357 nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
359 if (result == NSFileHandlingPanelCancelButton)
362 // Converts data from a NSArray of NSURL to the returned format.
363 // We should be careful to not call [thePanel URLs] more than once given that
364 // it creates a new array each time.
365 // We are using Fast Enumeration, thus the NSURL array is created once then
367 for (NSURL* url in [thePanel URLs]) {
372 nsCOMPtr<nsIFile> localFile;
373 NS_NewLocalFile(EmptyString(), true, getter_AddRefs(localFile));
374 nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
375 if (macLocalFile && NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)url))) {
376 outFiles.AppendObject(localFile);
380 if (outFiles.Count() > 0)
385 NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0);
388 // Use OpenPanel to do a GetFolder. Returns |returnOK| if the user presses OK in the dialog.
390 nsFilePicker::GetLocalFolder(const nsString& inTitle, nsIFile** outFile)
392 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
393 NS_ASSERTION(outFile, "this protected member function expects a null initialized out pointer");
395 int16_t retVal = (int16_t)returnCancel;
396 NSOpenPanel *thePanel = [NSOpenPanel openPanel];
398 SetShowHiddenFileState(thePanel);
400 // Set the options for how the get file dialog will appear
401 SetDialogTitle(inTitle, thePanel);
402 [thePanel setAllowsMultipleSelection:NO]; //this is default -probably doesn't need to be set
403 [thePanel setCanSelectHiddenExtension:YES];
404 [thePanel setCanChooseDirectories:YES];
405 [thePanel setCanChooseFiles:NO];
406 [thePanel setResolvesAliases:YES]; //this is default - probably doesn't need to be set
407 [thePanel setCanCreateDirectories:YES];
409 // packages != folders
410 [thePanel setTreatsFilePackagesAsDirectories:NO];
412 // set up default directory
413 NSString *theDir = PanelDefaultDirectory();
415 [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
417 nsCocoaUtils::PrepareForNativeAppModalDialog();
418 int result = [thePanel runModal];
419 nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
421 if (result == NSFileHandlingPanelCancelButton)
424 // get the path for the folder (we allow just 1, so that's all we get)
425 NSURL *theURL = [[thePanel URLs] objectAtIndex:0];
427 nsCOMPtr<nsIFile> localFile;
428 NS_NewLocalFile(EmptyString(), true, getter_AddRefs(localFile));
429 nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
430 if (macLocalFile && NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)theURL))) {
431 *outFile = localFile;
439 NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0);
442 // Returns |returnOK| if the user presses OK in the dialog.
444 nsFilePicker::PutLocalFile(const nsString& inTitle, const nsString& inDefaultName, nsIFile** outFile)
446 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
447 NS_ASSERTION(outFile, "this protected member function expects a null initialized out pointer");
449 int16_t retVal = returnCancel;
450 NSSavePanel *thePanel = [NSSavePanel savePanel];
452 SetShowHiddenFileState(thePanel);
454 SetDialogTitle(inTitle, thePanel);
456 // set up accessory view for file format options
457 NSView* accessoryView = GetAccessoryView();
458 [thePanel setAccessoryView:accessoryView];
460 // set up default file name
461 NSString* defaultFilename = [NSString stringWithCharacters:(const unichar*)inDefaultName.get() length:inDefaultName.Length()];
463 // set up default directory
464 NSString *theDir = PanelDefaultDirectory();
466 [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
470 nsCocoaUtils::PrepareForNativeAppModalDialog();
471 [thePanel setNameFieldStringValue:defaultFilename];
472 int result = [thePanel runModal];
473 nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
474 if (result == NSFileHandlingPanelCancelButton)
478 NSPopUpButton* popupButton = [accessoryView viewWithTag:kSaveTypeControlTag];
480 mSelectedTypeIndex = [popupButton indexOfSelectedItem];
483 NSURL* fileURL = [thePanel URL];
485 nsCOMPtr<nsIFile> localFile;
486 NS_NewLocalFile(EmptyString(), true, getter_AddRefs(localFile));
487 nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
488 if (macLocalFile && NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)fileURL))) {
489 *outFile = localFile;
491 // We tell if we are replacing or not by just looking to see if the file exists.
492 // The user could not have hit OK and not meant to replace the file.
493 if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]])
494 retVal = returnReplace;
502 NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0);
506 nsFilePicker::GetFilterList()
508 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
510 if (!mFilters.Length()) {
514 if (mFilters.Length() <= (uint32_t)mSelectedTypeIndex) {
515 NS_WARNING("An out of range index has been selected. Using the first index instead.");
516 mSelectedTypeIndex = 0;
519 const nsString& filterWide = mFilters[mSelectedTypeIndex];
520 if (!filterWide.Length()) {
524 if (filterWide.Equals(NS_LITERAL_STRING("*"))) {
528 // The extensions in filterWide are in the format "*.ext" but are expected
529 // in the format "ext" by NSOpenPanel. So we need to filter some characters.
530 NSMutableString* filterString = [[[NSMutableString alloc] initWithString:
531 [NSString stringWithCharacters:reinterpret_cast<const unichar*>(filterWide.get())
532 length:filterWide.Length()]] autorelease];
533 NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@". *"];
534 NSRange range = [filterString rangeOfCharacterFromSet:set];
535 while (range.length) {
536 [filterString replaceCharactersInRange:range withString:@""];
537 range = [filterString rangeOfCharacterFromSet:set];
540 return [[[NSArray alloc] initWithArray:
541 [filterString componentsSeparatedByString:@";"]] autorelease];
543 NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
546 // Sets the dialog title to whatever it should be. If it fails, eh,
547 // the OS will provide a sensible default.
549 nsFilePicker::SetDialogTitle(const nsString& inTitle, id aPanel)
551 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
553 [aPanel setTitle:[NSString stringWithCharacters:(const unichar*)inTitle.get() length:inTitle.Length()]];
555 NS_OBJC_END_TRY_ABORT_BLOCK;
558 // Converts path from an nsIFile into a NSString path
559 // If it fails, returns an empty string.
561 nsFilePicker::PanelDefaultDirectory()
563 NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
565 NSString *directory = nil;
566 if (mDisplayDirectory) {
567 nsAutoString pathStr;
568 mDisplayDirectory->GetPath(pathStr);
569 directory = [[[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(pathStr.get())
570 length:pathStr.Length()] autorelease];
574 NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
577 NS_IMETHODIMP nsFilePicker::GetFile(nsIFile **aFile)
579 NS_ENSURE_ARG_POINTER(aFile);
582 // just return the first file
583 if (mFiles.Count() > 0) {
584 *aFile = mFiles.ObjectAt(0);
585 NS_IF_ADDREF(*aFile);
591 NS_IMETHODIMP nsFilePicker::GetFileURL(nsIURI **aFileURL)
593 NS_ENSURE_ARG_POINTER(aFileURL);
596 if (mFiles.Count() == 0)
599 return NS_NewFileURI(aFileURL, mFiles.ObjectAt(0));
602 NS_IMETHODIMP nsFilePicker::GetFiles(nsISimpleEnumerator **aFiles)
604 return NS_NewArrayEnumerator(aFiles, mFiles);
607 NS_IMETHODIMP nsFilePicker::SetDefaultString(const nsAString& aString)
613 NS_IMETHODIMP nsFilePicker::GetDefaultString(nsAString& aString)
615 return NS_ERROR_FAILURE;
618 // The default extension to use for files
619 NS_IMETHODIMP nsFilePicker::GetDefaultExtension(nsAString& aExtension)
621 aExtension.Truncate();
625 NS_IMETHODIMP nsFilePicker::SetDefaultExtension(const nsAString& aExtension)
630 // Append an entry to the filters array
632 nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter)
634 // "..apps" has to be translated with native executable extensions.
635 if (aFilter.EqualsLiteral("..apps")) {
636 mFilters.AppendElement(NS_LITERAL_STRING("*.app"));
638 mFilters.AppendElement(aFilter);
640 mTitles.AppendElement(aTitle);
645 // Get the filter index - do we still need this?
646 NS_IMETHODIMP nsFilePicker::GetFilterIndex(int32_t *aFilterIndex)
648 *aFilterIndex = mSelectedTypeIndex;
652 // Set the filter index - do we still need this?
653 NS_IMETHODIMP nsFilePicker::SetFilterIndex(int32_t aFilterIndex)
655 mSelectedTypeIndex = aFilterIndex;