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"
13 #include "nsILocalFileMac.h"
14 #include "nsArrayEnumerator.h"
15 #include "nsIStringBundle.h"
16 #include "nsCocoaUtils.h"
17 #include "mozilla/Preferences.h"
19 // This must be included last:
20 #include "nsObjCExceptions.h"
22 using namespace mozilla;
24 const float kAccessoryViewPadding = 5;
25 const int kSaveTypeControlTag = 1;
27 static bool gCallSecretHiddenFileAPI = false;
28 const char kShowHiddenFilesPref[] = "filepicker.showHiddenFiles";
31 * This class is an observer of NSPopUpButton selection change.
33 @interface NSPopUpButtonObserver : NSObject {
34 NSPopUpButton* mPopUpButton;
35 NSOpenPanel* mOpenPanel;
36 nsFilePicker* mFilePicker;
38 - (void)setPopUpButton:(NSPopUpButton*)aPopUpButton;
39 - (void)setOpenPanel:(NSOpenPanel*)aOpenPanel;
40 - (void)setFilePicker:(nsFilePicker*)aFilePicker;
41 - (void)menuChangedItem:(NSNotification*)aSender;
44 NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
46 // We never want to call the secret show hidden files API unless the pref
47 // has been set. Once the pref has been set we always need to call it even
48 // if it disappears so that we stop showing hidden files if a user deletes
49 // the pref. If the secret API was used once and things worked out it should
50 // continue working for subsequent calls so the user is at no more risk.
51 static void SetShowHiddenFileState(NSSavePanel* panel) {
52 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
55 if (NS_SUCCEEDED(Preferences::GetBool(kShowHiddenFilesPref, &show))) {
56 gCallSecretHiddenFileAPI = true;
59 if (gCallSecretHiddenFileAPI) {
60 // invoke a method to get a Cocoa-internal nav view
61 SEL navViewSelector = @selector(_navView);
62 NSMethodSignature* navViewSignature =
63 [panel methodSignatureForSelector:navViewSelector];
64 if (!navViewSignature) return;
65 NSInvocation* navViewInvocation =
66 [NSInvocation invocationWithMethodSignature:navViewSignature];
67 [navViewInvocation setSelector:navViewSelector];
68 [navViewInvocation setTarget:panel];
69 [navViewInvocation invoke];
71 // get the returned nav view
73 [navViewInvocation getReturnValue:&navView];
75 // invoke the secret show hidden file state method on the nav view
76 SEL showHiddenFilesSelector = @selector(setShowsHiddenFiles:);
77 NSMethodSignature* showHiddenFilesSignature =
78 [navView methodSignatureForSelector:showHiddenFilesSelector];
79 if (!showHiddenFilesSignature) return;
80 NSInvocation* showHiddenFilesInvocation =
81 [NSInvocation invocationWithMethodSignature:showHiddenFilesSignature];
82 [showHiddenFilesInvocation setSelector:showHiddenFilesSelector];
83 [showHiddenFilesInvocation setTarget:navView];
84 [showHiddenFilesInvocation setArgument:&show atIndex:2];
85 [showHiddenFilesInvocation invoke];
88 NS_OBJC_END_TRY_IGNORE_BLOCK;
91 nsFilePicker::nsFilePicker() : mSelectedTypeIndex(0) {}
93 nsFilePicker::~nsFilePicker() {}
95 void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
99 NSView* nsFilePicker::GetAccessoryView() {
100 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
102 NSView* accessoryView =
103 [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)] autorelease];
105 // Set a label's default value.
106 NSString* label = @"Format:";
108 // Try to get the localized string.
109 nsCOMPtr<nsIStringBundleService> sbs =
110 do_GetService(NS_STRINGBUNDLE_CONTRACTID);
111 nsCOMPtr<nsIStringBundle> bundle;
112 nsresult rv = sbs->CreateBundle(
113 "chrome://global/locale/filepicker.properties", getter_AddRefs(bundle));
114 if (NS_SUCCEEDED(rv)) {
115 nsAutoString locaLabel;
116 rv = bundle->GetStringFromName("formatLabel", locaLabel);
117 if (NS_SUCCEEDED(rv)) {
119 stringWithCharacters:reinterpret_cast<const unichar*>(locaLabel.get())
120 length:locaLabel.Length()];
124 // set up label text field
125 NSTextField* textField = [[[NSTextField alloc] init] autorelease];
126 [textField setEditable:NO];
127 [textField setSelectable:NO];
128 [textField setDrawsBackground:NO];
129 [textField setBezeled:NO];
130 [textField setBordered:NO];
131 [textField setFont:[NSFont labelFontOfSize:13.0]];
132 [textField setStringValue:label];
133 [textField setTag:0];
134 [textField sizeToFit];
136 // set up popup button
137 NSPopUpButton* popupButton =
138 [[[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)
139 pullsDown:NO] autorelease];
140 uint32_t numMenuItems = mTitles.Length();
141 for (uint32_t i = 0; i < numMenuItems; i++) {
142 const nsString& currentTitle = mTitles[i];
143 NSString* titleString;
144 if (currentTitle.IsEmpty()) {
145 const nsString& currentFilter = mFilters[i];
147 [[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(
149 length:currentFilter.Length()];
152 [[NSString alloc] initWithCharacters:reinterpret_cast<const unichar*>(
154 length:currentTitle.Length()];
156 [popupButton addItemWithTitle:titleString];
157 [titleString release];
159 if (mSelectedTypeIndex >= 0 && (uint32_t)mSelectedTypeIndex < numMenuItems)
160 [popupButton selectItemAtIndex:mSelectedTypeIndex];
161 [popupButton setTag:kSaveTypeControlTag];
162 [popupButton sizeToFit]; // we have to do sizeToFit to get the height
164 // This is just a default width that works well, doesn't truncate the vast
165 // majority of things that might end up in the menu.
166 [popupButton setFrameSize:NSMakeSize(180, [popupButton frame].size.height)];
168 // position everything based on control sizes with kAccessoryViewPadding pix
169 // padding on each side kAccessoryViewPadding pix horizontal padding between
171 float greatestHeight = [textField frame].size.height;
172 if ([popupButton frame].size.height > greatestHeight)
173 greatestHeight = [popupButton frame].size.height;
174 float totalViewHeight = greatestHeight + kAccessoryViewPadding * 2;
175 float totalViewWidth = [textField frame].size.width +
176 [popupButton frame].size.width +
177 kAccessoryViewPadding * 3;
178 [accessoryView setFrameSize:NSMakeSize(totalViewWidth, totalViewHeight)];
180 float textFieldOriginY =
181 ((greatestHeight - [textField frame].size.height) / 2 + 1) +
182 kAccessoryViewPadding;
184 setFrameOrigin:NSMakePoint(kAccessoryViewPadding, textFieldOriginY)];
186 float popupOriginX = [textField frame].size.width + kAccessoryViewPadding * 2;
188 ((greatestHeight - [popupButton frame].size.height) / 2) +
189 kAccessoryViewPadding;
190 [popupButton setFrameOrigin:NSMakePoint(popupOriginX, popupOriginY)];
192 [accessoryView addSubview:textField];
193 [accessoryView addSubview:popupButton];
194 return accessoryView;
196 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
199 // Display the file dialog
200 nsresult nsFilePicker::Show(ResultCode* retval) {
201 NS_ENSURE_ARG_POINTER(retval);
203 *retval = returnCancel;
205 ResultCode userClicksOK = returnCancel;
208 nsCOMPtr<nsIFile> theFile;
210 // Note that GetLocalFolder shares a lot of code with GetLocalFiles.
211 // Could combine the functions and just pass the mode in.
214 userClicksOK = GetLocalFiles(false, mFiles);
217 case modeOpenMultiple:
218 userClicksOK = GetLocalFiles(true, mFiles);
222 userClicksOK = PutLocalFile(getter_AddRefs(theFile));
226 userClicksOK = GetLocalFolder(getter_AddRefs(theFile));
230 NS_ERROR("Unknown file picker mode");
234 if (theFile) mFiles.AppendObject(theFile);
236 *retval = userClicksOK;
240 static void UpdatePanelFileTypes(NSOpenPanel* aPanel, NSArray* aFilters) {
241 // If we show all file types, also "expose" bundles' contents.
242 [aPanel setTreatsFilePackagesAsDirectories:!aFilters];
244 [aPanel setAllowedFileTypes:aFilters];
247 @implementation NSPopUpButtonObserver
248 - (void)setPopUpButton:(NSPopUpButton*)aPopUpButton {
249 mPopUpButton = aPopUpButton;
252 - (void)setOpenPanel:(NSOpenPanel*)aOpenPanel {
253 mOpenPanel = aOpenPanel;
256 - (void)setFilePicker:(nsFilePicker*)aFilePicker {
257 mFilePicker = aFilePicker;
260 - (void)menuChangedItem:(NSNotification*)aSender {
261 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
262 int32_t selectedItem = [mPopUpButton indexOfSelectedItem];
263 if (selectedItem < 0) {
267 mFilePicker->SetFilterIndex(selectedItem);
268 UpdatePanelFileTypes(mOpenPanel, mFilePicker->GetFilterList());
270 NS_OBJC_END_TRY_BLOCK_RETURN();
274 // Use OpenPanel to do a GetFile. Returns |returnOK| if the user presses OK in
276 nsIFilePicker::ResultCode nsFilePicker::GetLocalFiles(
277 bool inAllowMultiple, nsCOMArray<nsIFile>& outFiles) {
278 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
280 ResultCode retVal = nsIFilePicker::returnCancel;
281 NSOpenPanel* thePanel = [NSOpenPanel openPanel];
283 SetShowHiddenFileState(thePanel);
285 // Set the options for how the get file dialog will appear
286 SetDialogTitle(mTitle, thePanel);
287 [thePanel setAllowsMultipleSelection:inAllowMultiple];
288 [thePanel setCanSelectHiddenExtension:YES];
289 [thePanel setCanChooseDirectories:NO];
290 [thePanel setCanChooseFiles:YES];
291 [thePanel setResolvesAliases:YES];
294 // filters may be null, if we should allow all file types.
295 NSArray* filters = GetFilterList();
297 // set up default directory
298 NSString* theDir = PanelDefaultDirectory();
300 // if this is the "Choose application..." dialog, and no other start
301 // dir has been set, then use the Applications folder.
303 if (filters && [filters count] == 1 &&
304 [(NSString*)[filters objectAtIndex:0] isEqualToString:@"app"])
305 theDir = @"/Applications/";
311 [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
315 nsCocoaUtils::PrepareForNativeAppModalDialog();
316 if (mFilters.Length() > 1) {
317 // [NSURL initWithString:] (below) throws an exception if URLString is nil.
319 NSPopUpButtonObserver* observer = [[NSPopUpButtonObserver alloc] init];
321 NSView* accessoryView = GetAccessoryView();
322 [thePanel setAccessoryView:accessoryView];
324 [observer setPopUpButton:[accessoryView viewWithTag:kSaveTypeControlTag]];
325 [observer setOpenPanel:thePanel];
326 [observer setFilePicker:this];
328 [[NSNotificationCenter defaultCenter]
330 selector:@selector(menuChangedItem:)
331 name:NSMenuWillSendActionNotification
334 UpdatePanelFileTypes(thePanel, filters);
335 result = [thePanel runModal];
337 [[NSNotificationCenter defaultCenter] removeObserver:observer];
340 // If we show all file types, also "expose" bundles' contents.
342 [thePanel setTreatsFilePackagesAsDirectories:YES];
344 [thePanel setAllowedFileTypes:filters];
345 result = [thePanel runModal];
347 nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
349 if (result == NSModalResponseCancel) return retVal;
351 // Converts data from a NSArray of NSURL to the returned format.
352 // We should be careful to not call [thePanel URLs] more than once given that
353 // it creates a new array each time.
354 // We are using Fast Enumeration, thus the NSURL array is created once then
356 for (NSURL* url in [thePanel URLs]) {
361 nsCOMPtr<nsIFile> localFile;
362 NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile));
363 nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
365 NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)url))) {
366 outFiles.AppendObject(localFile);
370 if (outFiles.Count() > 0) retVal = returnOK;
374 NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnOK);
377 // Use OpenPanel to do a GetFolder. Returns |returnOK| if the user presses OK in
379 nsIFilePicker::ResultCode nsFilePicker::GetLocalFolder(nsIFile** outFile) {
380 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
383 "this protected member function expects a null initialized out pointer");
385 ResultCode retVal = nsIFilePicker::returnCancel;
386 NSOpenPanel* thePanel = [NSOpenPanel openPanel];
388 SetShowHiddenFileState(thePanel);
390 // Set the options for how the get file dialog will appear
391 SetDialogTitle(mTitle, thePanel);
392 [thePanel setAllowsMultipleSelection:NO];
393 [thePanel setCanSelectHiddenExtension:YES];
394 [thePanel setCanChooseDirectories:YES];
395 [thePanel setCanChooseFiles:NO];
396 [thePanel setResolvesAliases:YES];
397 [thePanel setCanCreateDirectories:YES];
399 // packages != folders
400 [thePanel setTreatsFilePackagesAsDirectories:NO];
402 // set up default directory
403 NSString* theDir = PanelDefaultDirectory();
405 [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
407 nsCocoaUtils::PrepareForNativeAppModalDialog();
408 int result = [thePanel runModal];
409 nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
411 if (result == NSModalResponseCancel) return retVal;
413 // get the path for the folder (we allow just 1, so that's all we get)
414 NSURL* theURL = [[thePanel URLs] objectAtIndex:0];
416 nsCOMPtr<nsIFile> localFile;
417 NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile));
418 nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
420 NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)theURL))) {
421 *outFile = localFile;
429 NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnOK);
432 // Returns |returnOK| if the user presses OK in the dialog.
433 nsIFilePicker::ResultCode nsFilePicker::PutLocalFile(nsIFile** outFile) {
434 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
437 "this protected member function expects a null initialized out pointer");
439 ResultCode retVal = nsIFilePicker::returnCancel;
440 NSSavePanel* thePanel = [NSSavePanel savePanel];
442 SetShowHiddenFileState(thePanel);
444 SetDialogTitle(mTitle, thePanel);
446 // set up accessory view for file format options
447 NSView* accessoryView = GetAccessoryView();
448 [thePanel setAccessoryView:accessoryView];
450 // set up default file name
451 NSString* defaultFilename =
452 [NSString stringWithCharacters:(const unichar*)mDefaultFilename.get()
453 length:mDefaultFilename.Length()];
455 // Set up the allowed type. This prevents the extension from being selected.
456 NSString* extension = defaultFilename.pathExtension;
457 if (extension.length != 0) {
458 thePanel.allowedFileTypes = @[ extension ];
460 // Allow users to change the extension.
461 thePanel.allowsOtherFileTypes = YES;
463 // If extensions are hidden and we’re saving a file with multiple extensions,
464 // only the last extension will be hidden in the panel (".tar.gz" will become
465 // ".tar"). If the remaining extension is known, the OS will think that we're
466 // trying to add a non-default extension. To avoid the confusion, we ensure
467 // that all extensions are shown in the panel if the remaining extension is
470 [[defaultFilename lastPathComponent] stringByDeletingPathExtension];
471 NSString* otherExtension = fileName.pathExtension;
472 if (otherExtension.length != 0) {
473 // There's another extension here. Get the UTI.
474 CFStringRef type = UTTypeCreatePreferredIdentifierForTag(
475 kUTTagClassFilenameExtension, (CFStringRef)otherExtension, NULL);
477 if (!CFStringHasPrefix(type, CFSTR("dyn."))) {
478 // We have a UTI, otherwise the type would have a "dyn." prefix. Ensure
479 // extensions are shown in the panel.
480 [thePanel setExtensionHidden:NO];
486 // set up default directory
487 NSString* theDir = PanelDefaultDirectory();
489 [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]];
493 nsCocoaUtils::PrepareForNativeAppModalDialog();
494 [thePanel setNameFieldStringValue:defaultFilename];
495 int result = [thePanel runModal];
496 nsCocoaUtils::CleanUpAfterNativeAppModalDialog();
497 if (result == NSModalResponseCancel) return retVal;
500 NSPopUpButton* popupButton = [accessoryView viewWithTag:kSaveTypeControlTag];
502 mSelectedTypeIndex = [popupButton indexOfSelectedItem];
505 NSURL* fileURL = [thePanel URL];
507 nsCOMPtr<nsIFile> localFile;
508 NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile));
509 nsCOMPtr<nsILocalFileMac> macLocalFile = do_QueryInterface(localFile);
511 NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)fileURL))) {
512 *outFile = localFile;
514 // We tell if we are replacing or not by just looking to see if the file
515 // exists. The user could not have hit OK and not meant to replace the
517 if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]])
518 retVal = returnReplace;
526 NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnCancel);
529 NSArray* nsFilePicker::GetFilterList() {
530 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
532 if (!mFilters.Length()) {
536 if (mFilters.Length() <= (uint32_t)mSelectedTypeIndex) {
537 NS_WARNING("An out of range index has been selected. Using the first index "
539 mSelectedTypeIndex = 0;
542 const nsString& filterWide = mFilters[mSelectedTypeIndex];
543 if (!filterWide.Length()) {
547 if (filterWide.Equals(u"*"_ns)) {
551 // The extensions in filterWide are in the format "*.ext" but are expected
552 // in the format "ext" by NSOpenPanel. So we need to filter some characters.
553 NSMutableString* filterString = [[[NSMutableString alloc]
554 initWithString:[NSString
555 stringWithCharacters:reinterpret_cast<const unichar*>(
557 length:filterWide.Length()]]
559 NSCharacterSet* set =
560 [NSCharacterSet characterSetWithCharactersInString:@". *"];
561 NSRange range = [filterString rangeOfCharacterFromSet:set];
562 while (range.length) {
563 [filterString replaceCharactersInRange:range withString:@""];
564 range = [filterString rangeOfCharacterFromSet:set];
567 return [[[NSArray alloc]
568 initWithArray:[filterString componentsSeparatedByString:@";"]]
571 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
574 // Sets the dialog title to whatever it should be. If it fails, eh,
575 // the OS will provide a sensible default.
576 void nsFilePicker::SetDialogTitle(const nsString& inTitle, id aPanel) {
577 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
579 [aPanel setTitle:[NSString stringWithCharacters:(const unichar*)inTitle.get()
580 length:inTitle.Length()]];
582 if (!mOkButtonLabel.IsEmpty()) {
585 stringWithCharacters:(const unichar*)mOkButtonLabel.get()
586 length:mOkButtonLabel.Length()]];
589 NS_OBJC_END_TRY_IGNORE_BLOCK;
592 // Converts path from an nsIFile into a NSString path
593 // If it fails, returns an empty string.
594 NSString* nsFilePicker::PanelDefaultDirectory() {
595 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
597 NSString* directory = nil;
598 if (mDisplayDirectory) {
599 nsAutoString pathStr;
600 mDisplayDirectory->GetPath(pathStr);
601 directory = [[[NSString alloc]
602 initWithCharacters:reinterpret_cast<const unichar*>(pathStr.get())
603 length:pathStr.Length()] autorelease];
607 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
610 NS_IMETHODIMP nsFilePicker::GetFile(nsIFile** aFile) {
611 NS_ENSURE_ARG_POINTER(aFile);
614 // just return the first file
615 if (mFiles.Count() > 0) {
616 *aFile = mFiles.ObjectAt(0);
617 NS_IF_ADDREF(*aFile);
623 NS_IMETHODIMP nsFilePicker::GetFileURL(nsIURI** aFileURL) {
624 NS_ENSURE_ARG_POINTER(aFileURL);
627 if (mFiles.Count() == 0) return NS_OK;
629 return NS_NewFileURI(aFileURL, mFiles.ObjectAt(0));
632 NS_IMETHODIMP nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
633 return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
636 NS_IMETHODIMP nsFilePicker::SetDefaultString(const nsAString& aString) {
637 mDefaultFilename = aString;
641 NS_IMETHODIMP nsFilePicker::GetDefaultString(nsAString& aString) {
642 return NS_ERROR_FAILURE;
645 // The default extension to use for files
646 NS_IMETHODIMP nsFilePicker::GetDefaultExtension(nsAString& aExtension) {
647 aExtension.Truncate();
651 NS_IMETHODIMP nsFilePicker::SetDefaultExtension(const nsAString& aExtension) {
655 // Append an entry to the filters array
657 nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
658 // "..apps" has to be translated with native executable extensions.
659 if (aFilter.EqualsLiteral("..apps")) {
660 mFilters.AppendElement(u"*.app"_ns);
662 mFilters.AppendElement(aFilter);
664 mTitles.AppendElement(aTitle);
669 // Get the filter index - do we still need this?
670 NS_IMETHODIMP nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
671 *aFilterIndex = mSelectedTypeIndex;
675 // Set the filter index - do we still need this?
676 NS_IMETHODIMP nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
677 mSelectedTypeIndex = aFilterIndex;