/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #import #include "nsFilePicker.h" #include "nsCOMPtr.h" #include "nsReadableUtils.h" #include "nsNetUtil.h" #include "nsIFile.h" #include "nsILocalFileMac.h" #include "nsArrayEnumerator.h" #include "nsIStringBundle.h" #include "nsCocoaUtils.h" #include "mozilla/Preferences.h" // This must be included last: #include "nsObjCExceptions.h" using namespace mozilla; const float kAccessoryViewPadding = 5; const int kSaveTypeControlTag = 1; static bool gCallSecretHiddenFileAPI = false; const char kShowHiddenFilesPref[] = "filepicker.showHiddenFiles"; /** * This class is an observer of NSPopUpButton selection change. */ @interface NSPopUpButtonObserver : NSObject { NSPopUpButton* mPopUpButton; NSOpenPanel* mOpenPanel; nsFilePicker* mFilePicker; } - (void)setPopUpButton:(NSPopUpButton*)aPopUpButton; - (void)setOpenPanel:(NSOpenPanel*)aOpenPanel; - (void)setFilePicker:(nsFilePicker*)aFilePicker; - (void)menuChangedItem:(NSNotification*)aSender; @end NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) // We never want to call the secret show hidden files API unless the pref // has been set. Once the pref has been set we always need to call it even // if it disappears so that we stop showing hidden files if a user deletes // the pref. If the secret API was used once and things worked out it should // continue working for subsequent calls so the user is at no more risk. static void SetShowHiddenFileState(NSSavePanel* panel) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; bool show = false; if (NS_SUCCEEDED(Preferences::GetBool(kShowHiddenFilesPref, &show))) { gCallSecretHiddenFileAPI = true; } if (gCallSecretHiddenFileAPI) { // invoke a method to get a Cocoa-internal nav view SEL navViewSelector = @selector(_navView); NSMethodSignature* navViewSignature = [panel methodSignatureForSelector:navViewSelector]; if (!navViewSignature) return; NSInvocation* navViewInvocation = [NSInvocation invocationWithMethodSignature:navViewSignature]; [navViewInvocation setSelector:navViewSelector]; [navViewInvocation setTarget:panel]; [navViewInvocation invoke]; // get the returned nav view id navView = nil; [navViewInvocation getReturnValue:&navView]; // invoke the secret show hidden file state method on the nav view SEL showHiddenFilesSelector = @selector(setShowsHiddenFiles:); NSMethodSignature* showHiddenFilesSignature = [navView methodSignatureForSelector:showHiddenFilesSelector]; if (!showHiddenFilesSignature) return; NSInvocation* showHiddenFilesInvocation = [NSInvocation invocationWithMethodSignature:showHiddenFilesSignature]; [showHiddenFilesInvocation setSelector:showHiddenFilesSelector]; [showHiddenFilesInvocation setTarget:navView]; [showHiddenFilesInvocation setArgument:&show atIndex:2]; [showHiddenFilesInvocation invoke]; } NS_OBJC_END_TRY_IGNORE_BLOCK; } nsFilePicker::nsFilePicker() : mSelectedTypeIndex(0) {} nsFilePicker::~nsFilePicker() {} void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) { mTitle = aTitle; } NSView* nsFilePicker::GetAccessoryView() { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; NSView* accessoryView = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)] autorelease]; // Set a label's default value. NSString* label = @"Format:"; // Try to get the localized string. nsCOMPtr sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID); nsCOMPtr bundle; nsresult rv = sbs->CreateBundle( "chrome://global/locale/filepicker.properties", getter_AddRefs(bundle)); if (NS_SUCCEEDED(rv)) { nsAutoString locaLabel; rv = bundle->GetStringFromName("formatLabel", locaLabel); if (NS_SUCCEEDED(rv)) { label = [NSString stringWithCharacters:reinterpret_cast(locaLabel.get()) length:locaLabel.Length()]; } } // set up label text field NSTextField* textField = [[[NSTextField alloc] init] autorelease]; [textField setEditable:NO]; [textField setSelectable:NO]; [textField setDrawsBackground:NO]; [textField setBezeled:NO]; [textField setBordered:NO]; [textField setFont:[NSFont labelFontOfSize:13.0]]; [textField setStringValue:label]; [textField setTag:0]; [textField sizeToFit]; // set up popup button NSPopUpButton* popupButton = [[[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 0, 0, 0) pullsDown:NO] autorelease]; uint32_t numMenuItems = mTitles.Length(); for (uint32_t i = 0; i < numMenuItems; i++) { const nsString& currentTitle = mTitles[i]; NSString* titleString; if (currentTitle.IsEmpty()) { const nsString& currentFilter = mFilters[i]; titleString = [[NSString alloc] initWithCharacters:reinterpret_cast( currentFilter.get()) length:currentFilter.Length()]; } else { titleString = [[NSString alloc] initWithCharacters:reinterpret_cast( currentTitle.get()) length:currentTitle.Length()]; } [popupButton addItemWithTitle:titleString]; [titleString release]; } if (mSelectedTypeIndex >= 0 && (uint32_t)mSelectedTypeIndex < numMenuItems) [popupButton selectItemAtIndex:mSelectedTypeIndex]; [popupButton setTag:kSaveTypeControlTag]; [popupButton sizeToFit]; // we have to do sizeToFit to get the height // calculated for us // This is just a default width that works well, doesn't truncate the vast // majority of things that might end up in the menu. [popupButton setFrameSize:NSMakeSize(180, [popupButton frame].size.height)]; // position everything based on control sizes with kAccessoryViewPadding pix // padding on each side kAccessoryViewPadding pix horizontal padding between // controls float greatestHeight = [textField frame].size.height; if ([popupButton frame].size.height > greatestHeight) greatestHeight = [popupButton frame].size.height; float totalViewHeight = greatestHeight + kAccessoryViewPadding * 2; float totalViewWidth = [textField frame].size.width + [popupButton frame].size.width + kAccessoryViewPadding * 3; [accessoryView setFrameSize:NSMakeSize(totalViewWidth, totalViewHeight)]; float textFieldOriginY = ((greatestHeight - [textField frame].size.height) / 2 + 1) + kAccessoryViewPadding; [textField setFrameOrigin:NSMakePoint(kAccessoryViewPadding, textFieldOriginY)]; float popupOriginX = [textField frame].size.width + kAccessoryViewPadding * 2; float popupOriginY = ((greatestHeight - [popupButton frame].size.height) / 2) + kAccessoryViewPadding; [popupButton setFrameOrigin:NSMakePoint(popupOriginX, popupOriginY)]; [accessoryView addSubview:textField]; [accessoryView addSubview:popupButton]; return accessoryView; NS_OBJC_END_TRY_BLOCK_RETURN(nil); } // Display the file dialog nsresult nsFilePicker::Show(ResultCode* retval) { NS_ENSURE_ARG_POINTER(retval); *retval = returnCancel; ResultCode userClicksOK = returnCancel; mFiles.Clear(); nsCOMPtr theFile; // Note that GetLocalFolder shares a lot of code with GetLocalFiles. // Could combine the functions and just pass the mode in. switch (mMode) { case modeOpen: userClicksOK = GetLocalFiles(false, mFiles); break; case modeOpenMultiple: userClicksOK = GetLocalFiles(true, mFiles); break; case modeSave: userClicksOK = PutLocalFile(getter_AddRefs(theFile)); break; case modeGetFolder: userClicksOK = GetLocalFolder(getter_AddRefs(theFile)); break; default: NS_ERROR("Unknown file picker mode"); break; } if (theFile) mFiles.AppendObject(theFile); *retval = userClicksOK; return NS_OK; } static void UpdatePanelFileTypes(NSOpenPanel* aPanel, NSArray* aFilters) { // If we show all file types, also "expose" bundles' contents. [aPanel setTreatsFilePackagesAsDirectories:!aFilters]; [aPanel setAllowedFileTypes:aFilters]; } @implementation NSPopUpButtonObserver - (void)setPopUpButton:(NSPopUpButton*)aPopUpButton { mPopUpButton = aPopUpButton; } - (void)setOpenPanel:(NSOpenPanel*)aOpenPanel { mOpenPanel = aOpenPanel; } - (void)setFilePicker:(nsFilePicker*)aFilePicker { mFilePicker = aFilePicker; } - (void)menuChangedItem:(NSNotification*)aSender { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; int32_t selectedItem = [mPopUpButton indexOfSelectedItem]; if (selectedItem < 0) { return; } mFilePicker->SetFilterIndex(selectedItem); UpdatePanelFileTypes(mOpenPanel, mFilePicker->GetFilterList()); NS_OBJC_END_TRY_BLOCK_RETURN(); } @end // Use OpenPanel to do a GetFile. Returns |returnOK| if the user presses OK in // the dialog. nsIFilePicker::ResultCode nsFilePicker::GetLocalFiles( bool inAllowMultiple, nsCOMArray& outFiles) { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; ResultCode retVal = nsIFilePicker::returnCancel; NSOpenPanel* thePanel = [NSOpenPanel openPanel]; SetShowHiddenFileState(thePanel); // Set the options for how the get file dialog will appear SetDialogTitle(mTitle, thePanel); [thePanel setAllowsMultipleSelection:inAllowMultiple]; [thePanel setCanSelectHiddenExtension:YES]; [thePanel setCanChooseDirectories:NO]; [thePanel setCanChooseFiles:YES]; [thePanel setResolvesAliases:YES]; // Get filters // filters may be null, if we should allow all file types. NSArray* filters = GetFilterList(); // set up default directory NSString* theDir = PanelDefaultDirectory(); // if this is the "Choose application..." dialog, and no other start // dir has been set, then use the Applications folder. if (!theDir) { if (filters && [filters count] == 1 && [(NSString*)[filters objectAtIndex:0] isEqualToString:@"app"]) theDir = @"/Applications/"; else theDir = @""; } if (theDir) { [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]]; } int result; nsCocoaUtils::PrepareForNativeAppModalDialog(); if (mFilters.Length() > 1) { // [NSURL initWithString:] (below) throws an exception if URLString is nil. NSPopUpButtonObserver* observer = [[NSPopUpButtonObserver alloc] init]; NSView* accessoryView = GetAccessoryView(); [thePanel setAccessoryView:accessoryView]; [observer setPopUpButton:[accessoryView viewWithTag:kSaveTypeControlTag]]; [observer setOpenPanel:thePanel]; [observer setFilePicker:this]; [[NSNotificationCenter defaultCenter] addObserver:observer selector:@selector(menuChangedItem:) name:NSMenuWillSendActionNotification object:nil]; UpdatePanelFileTypes(thePanel, filters); result = [thePanel runModal]; [[NSNotificationCenter defaultCenter] removeObserver:observer]; [observer release]; } else { // If we show all file types, also "expose" bundles' contents. if (!filters) { [thePanel setTreatsFilePackagesAsDirectories:YES]; } [thePanel setAllowedFileTypes:filters]; result = [thePanel runModal]; } nsCocoaUtils::CleanUpAfterNativeAppModalDialog(); if (result == NSModalResponseCancel) return retVal; // Converts data from a NSArray of NSURL to the returned format. // We should be careful to not call [thePanel URLs] more than once given that // it creates a new array each time. // We are using Fast Enumeration, thus the NSURL array is created once then // iterated. for (NSURL* url in [thePanel URLs]) { if (!url) { continue; } nsCOMPtr localFile; NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile)); nsCOMPtr macLocalFile = do_QueryInterface(localFile); if (macLocalFile && NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)url))) { outFiles.AppendObject(localFile); } } if (outFiles.Count() > 0) retVal = returnOK; return retVal; NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnOK); } // Use OpenPanel to do a GetFolder. Returns |returnOK| if the user presses OK in // the dialog. nsIFilePicker::ResultCode nsFilePicker::GetLocalFolder(nsIFile** outFile) { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; NS_ASSERTION( outFile, "this protected member function expects a null initialized out pointer"); ResultCode retVal = nsIFilePicker::returnCancel; NSOpenPanel* thePanel = [NSOpenPanel openPanel]; SetShowHiddenFileState(thePanel); // Set the options for how the get file dialog will appear SetDialogTitle(mTitle, thePanel); [thePanel setAllowsMultipleSelection:NO]; [thePanel setCanSelectHiddenExtension:YES]; [thePanel setCanChooseDirectories:YES]; [thePanel setCanChooseFiles:NO]; [thePanel setResolvesAliases:YES]; [thePanel setCanCreateDirectories:YES]; // packages != folders [thePanel setTreatsFilePackagesAsDirectories:NO]; // set up default directory NSString* theDir = PanelDefaultDirectory(); if (theDir) { [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]]; } nsCocoaUtils::PrepareForNativeAppModalDialog(); int result = [thePanel runModal]; nsCocoaUtils::CleanUpAfterNativeAppModalDialog(); if (result == NSModalResponseCancel) return retVal; // get the path for the folder (we allow just 1, so that's all we get) NSURL* theURL = [[thePanel URLs] objectAtIndex:0]; if (theURL) { nsCOMPtr localFile; NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile)); nsCOMPtr macLocalFile = do_QueryInterface(localFile); if (macLocalFile && NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)theURL))) { *outFile = localFile; NS_ADDREF(*outFile); retVal = returnOK; } } return retVal; NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnOK); } // Returns |returnOK| if the user presses OK in the dialog. nsIFilePicker::ResultCode nsFilePicker::PutLocalFile(nsIFile** outFile) { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; NS_ASSERTION( outFile, "this protected member function expects a null initialized out pointer"); ResultCode retVal = nsIFilePicker::returnCancel; NSSavePanel* thePanel = [NSSavePanel savePanel]; SetShowHiddenFileState(thePanel); SetDialogTitle(mTitle, thePanel); // set up accessory view for file format options NSView* accessoryView = GetAccessoryView(); [thePanel setAccessoryView:accessoryView]; // set up default file name NSString* defaultFilename = [NSString stringWithCharacters:(const unichar*)mDefaultFilename.get() length:mDefaultFilename.Length()]; // Set up the allowed type. This prevents the extension from being selected. NSString* extension = defaultFilename.pathExtension; if (extension.length != 0) { thePanel.allowedFileTypes = @[ extension ]; } // Allow users to change the extension. thePanel.allowsOtherFileTypes = YES; // If extensions are hidden and we’re saving a file with multiple extensions, // only the last extension will be hidden in the panel (".tar.gz" will become // ".tar"). If the remaining extension is known, the OS will think that we're // trying to add a non-default extension. To avoid the confusion, we ensure // that all extensions are shown in the panel if the remaining extension is // known by the OS. NSString* fileName = [[defaultFilename lastPathComponent] stringByDeletingPathExtension]; NSString* otherExtension = fileName.pathExtension; if (otherExtension.length != 0) { // There's another extension here. Get the UTI. CFStringRef type = UTTypeCreatePreferredIdentifierForTag( kUTTagClassFilenameExtension, (CFStringRef)otherExtension, NULL); if (type) { if (!CFStringHasPrefix(type, CFSTR("dyn."))) { // We have a UTI, otherwise the type would have a "dyn." prefix. Ensure // extensions are shown in the panel. [thePanel setExtensionHidden:NO]; } CFRelease(type); } } // set up default directory NSString* theDir = PanelDefaultDirectory(); if (theDir) { [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]]; } // load the panel nsCocoaUtils::PrepareForNativeAppModalDialog(); [thePanel setNameFieldStringValue:defaultFilename]; int result = [thePanel runModal]; nsCocoaUtils::CleanUpAfterNativeAppModalDialog(); if (result == NSModalResponseCancel) return retVal; // get the save type NSPopUpButton* popupButton = [accessoryView viewWithTag:kSaveTypeControlTag]; if (popupButton) { mSelectedTypeIndex = [popupButton indexOfSelectedItem]; } NSURL* fileURL = [thePanel URL]; if (fileURL) { nsCOMPtr localFile; NS_NewLocalFile(u""_ns, true, getter_AddRefs(localFile)); nsCOMPtr macLocalFile = do_QueryInterface(localFile); if (macLocalFile && NS_SUCCEEDED(macLocalFile->InitWithCFURL((CFURLRef)fileURL))) { *outFile = localFile; NS_ADDREF(*outFile); // We tell if we are replacing or not by just looking to see if the file // exists. The user could not have hit OK and not meant to replace the // file. if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) retVal = returnReplace; else retVal = returnOK; } } return retVal; NS_OBJC_END_TRY_BLOCK_RETURN(nsIFilePicker::returnCancel); } NSArray* nsFilePicker::GetFilterList() { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; if (!mFilters.Length()) { return nil; } if (mFilters.Length() <= (uint32_t)mSelectedTypeIndex) { NS_WARNING("An out of range index has been selected. Using the first index " "instead."); mSelectedTypeIndex = 0; } const nsString& filterWide = mFilters[mSelectedTypeIndex]; if (!filterWide.Length()) { return nil; } if (filterWide.Equals(u"*"_ns)) { return nil; } // The extensions in filterWide are in the format "*.ext" but are expected // in the format "ext" by NSOpenPanel. So we need to filter some characters. NSMutableString* filterString = [[[NSMutableString alloc] initWithString:[NSString stringWithCharacters:reinterpret_cast( filterWide.get()) length:filterWide.Length()]] autorelease]; NSCharacterSet* set = [NSCharacterSet characterSetWithCharactersInString:@". *"]; NSRange range = [filterString rangeOfCharacterFromSet:set]; while (range.length) { [filterString replaceCharactersInRange:range withString:@""]; range = [filterString rangeOfCharacterFromSet:set]; } return [[[NSArray alloc] initWithArray:[filterString componentsSeparatedByString:@";"]] autorelease]; NS_OBJC_END_TRY_BLOCK_RETURN(nil); } // Sets the dialog title to whatever it should be. If it fails, eh, // the OS will provide a sensible default. void nsFilePicker::SetDialogTitle(const nsString& inTitle, id aPanel) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; [aPanel setTitle:[NSString stringWithCharacters:(const unichar*)inTitle.get() length:inTitle.Length()]]; if (!mOkButtonLabel.IsEmpty()) { [aPanel setPrompt:[NSString stringWithCharacters:(const unichar*)mOkButtonLabel.get() length:mOkButtonLabel.Length()]]; } NS_OBJC_END_TRY_IGNORE_BLOCK; } // Converts path from an nsIFile into a NSString path // If it fails, returns an empty string. NSString* nsFilePicker::PanelDefaultDirectory() { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; NSString* directory = nil; if (mDisplayDirectory) { nsAutoString pathStr; mDisplayDirectory->GetPath(pathStr); directory = [[[NSString alloc] initWithCharacters:reinterpret_cast(pathStr.get()) length:pathStr.Length()] autorelease]; } return directory; NS_OBJC_END_TRY_BLOCK_RETURN(nil); } NS_IMETHODIMP nsFilePicker::GetFile(nsIFile** aFile) { NS_ENSURE_ARG_POINTER(aFile); *aFile = nullptr; // just return the first file if (mFiles.Count() > 0) { *aFile = mFiles.ObjectAt(0); NS_IF_ADDREF(*aFile); } return NS_OK; } NS_IMETHODIMP nsFilePicker::GetFileURL(nsIURI** aFileURL) { NS_ENSURE_ARG_POINTER(aFileURL); *aFileURL = nullptr; if (mFiles.Count() == 0) return NS_OK; return NS_NewFileURI(aFileURL, mFiles.ObjectAt(0)); } NS_IMETHODIMP nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) { return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile)); } NS_IMETHODIMP nsFilePicker::SetDefaultString(const nsAString& aString) { mDefaultFilename = aString; return NS_OK; } NS_IMETHODIMP nsFilePicker::GetDefaultString(nsAString& aString) { return NS_ERROR_FAILURE; } // The default extension to use for files NS_IMETHODIMP nsFilePicker::GetDefaultExtension(nsAString& aExtension) { aExtension.Truncate(); return NS_OK; } NS_IMETHODIMP nsFilePicker::SetDefaultExtension(const nsAString& aExtension) { return NS_OK; } // Append an entry to the filters array NS_IMETHODIMP nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) { // "..apps" has to be translated with native executable extensions. if (aFilter.EqualsLiteral("..apps")) { mFilters.AppendElement(u"*.app"_ns); } else { mFilters.AppendElement(aFilter); } mTitles.AppendElement(aTitle); return NS_OK; } // Get the filter index - do we still need this? NS_IMETHODIMP nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) { *aFilterIndex = mSelectedTypeIndex; return NS_OK; } // Set the filter index - do we still need this? NS_IMETHODIMP nsFilePicker::SetFilterIndex(int32_t aFilterIndex) { mSelectedTypeIndex = aFilterIndex; return NS_OK; }