/* 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 "mozilla/Preferences.h" #include "nsArrayEnumerator.h" #include "nsCOMPtr.h" #include "nsCocoaUtils.h" #include "nsFilePicker.h" #include "nsIFile.h" #include "nsILocalFileMac.h" #include "nsIStringBundle.h" #include "nsNetUtil.h" #include "nsReadableUtils.h" #include "nsThreadUtils.h" // This must be included last: #include "nsObjCExceptions.h" using namespace mozilla; const float kAccessoryViewPadding = 5; const int kSaveTypeControlTag = 1; const char kShowHiddenFilesPref[] = "filepicker.showHiddenFiles"; // This class is an observer of NSPopUpButton selection change. @interface MOZFilePickerPopUpObserver : NSObject { NSPopUpButton* mPopUpButton; NSOpenPanel* mOpenPanel; RefPtr mFilePicker; } - (void)setPopUpButton:(NSPopUpButton*)aPopUpButton; - (void)setOpenPanel:(NSOpenPanel*)aOpenPanel; - (void)setFilePicker:(nsFilePicker*)aFilePicker; - (void)menuChangedItem:(NSNotification*)aSender; @end @interface MOZSaveFilePickerPopUpObserver : NSObject { NSPopUpButton* mPopUpButton; NSSavePanel* mSavePanel; RefPtr mFilePicker; } - (void)setPopUpButton:(NSPopUpButton*)aPopUpButton; - (void)setSavePanel:(NSSavePanel*)aSavePanel; - (void)setFilePicker:(nsFilePicker*)aFilePicker; - (void)menuChangedItem:(NSNotification*)aSender; @end // Panel delegate that ignores confirmations that arrive before the file // picker's input-protection time range has passed. Returning NO from // panel:validateURL:error: to keep the panel open is the documented purpose // of the method; leaving the error nil so no alert is shown is undocumented, // but observed to hold (including for the out-of-process panel). If a future // macOS changes this, the worst case is a stray alert or the check quietly // doing nothing, and it can be turned off with // security.notification_enable_delay. @interface MOZFilePickerInputProtector : NSObject { RefPtr mFilePicker; } - (id)initWithFilePicker:(nsFilePicker*)aFilePicker; @end @implementation MOZFilePickerInputProtector - (id)initWithFilePicker:(nsFilePicker*)aFilePicker { if ((self = [super init])) { mFilePicker = aFilePicker; } return self; } - (BOOL)panel:(id)sender validateURL:(NSURL*)url error:(NSError**)outError { // url is intentionally unused: the file we return is read from the panel // when it finally closes, so an ignored early confirmation can't pin a // stale selection. if (mFilePicker && mFilePicker->IsPickerInputProtected()) { if (outError) { *outError = nil; } return NO; } return YES; } @end NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) static void SetShowHiddenFileState(NSSavePanel* panel) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; bool show = false; if (NS_SUCCEEDED(Preferences::GetBool(kShowHiddenFilesPref, &show))) { [panel setShowsHiddenFiles:show]; } NS_OBJC_END_TRY_IGNORE_BLOCK; } // On macOS 26 (Tahoe), the panel's sheet completion handler can run before the // modal session has fully unwound. Invoking the callback synchronously here // would let a consumer open another modal on top of a session that is still // tearing down, which hangs (bug 2053177). Deferring to a fresh main-thread // turn lets the modal finish unwinding before the callback runs. static void InvokeFilePickerCallbackDeferred( nsIFilePickerShownCallback* aCallback, nsIFilePicker::ResultCode aResult) { if (!aCallback) { return; } nsCOMPtr callback = aCallback; NS_DispatchToMainThread(NS_NewRunnableFunction( "nsFilePicker::InvokeCallback", [callback, aResult]() { callback->Done(aResult); })); } nsFilePicker::nsFilePicker() = default; nsFilePicker::~nsFilePicker() = default; void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) { mParentWidget = aParent; 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 localizedLabel; rv = bundle->GetStringFromName("formatLabel", localizedLabel); if (NS_SUCCEEDED(rv)) { label = [NSString stringWithCharacters:reinterpret_cast( localizedLabel.get()) length:localizedLabel.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 && static_cast(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); } NS_IMETHODIMP nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) { if (MaybeBlockFilePicker(aCallback)) { return NS_OK; } RefPtr self = this; nsCOMPtr callback = aCallback; return NS_DispatchToMainThread( NS_NewRunnableFunction("nsFilePicker::Open", [self, callback]() mutable { switch (self->mMode) { case modeOpen: self->PresentOpenPanel(false, callback); break; case modeOpenMultiple: self->PresentOpenPanel(true, callback); break; case modeSave: self->PresentSavePanel(callback); break; case modeGetFolder: self->PresentFolderPanel(callback); break; default: NS_ERROR("Unknown file picker mode"); if (callback) { callback->Done(nsIFilePicker::returnCancel); } break; } })); } void nsFilePicker::BeginPanelAsync(NSSavePanel* aPanel, void (^aHandler)(NSModalResponse)) { NSWindow* parentWindow = nil; if (mParentWidget) { parentWindow = static_cast(mParentWidget->GetNativeData(NS_NATIVE_WINDOW)); } // Attach a delegate to ignore confirmations that arrive before the // input-protection time range has passed. The panel does not retain its // delegate, so the completion handler releases it. MOZFilePickerInputProtector* protector = [[MOZFilePickerInputProtector alloc] initWithFilePicker:this]; [aPanel setDelegate:protector]; void (^handler)(NSModalResponse) = ^(NSModalResponse result) { aHandler(result); [aPanel setDelegate:nil]; [protector release]; }; RecordLastShownTime(); if (parentWindow) { [aPanel beginSheetModalForWindow:parentWindow completionHandler:handler]; } else { [aPanel beginWithCompletionHandler:handler]; } } static void UpdatePanelFileTypes(NSOpenPanel* aPanel, NSArray* aFilters) { // If we show all file types, also "expose" bundles' contents. [aPanel setTreatsFilePackagesAsDirectories:!aFilters]; [aPanel setAllowedFileTypes:aFilters]; } @implementation MOZFilePickerPopUpObserver - (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_IGNORE_BLOCK; int32_t selectedItem = [mPopUpButton indexOfSelectedItem]; if (selectedItem < 0) { return; } mFilePicker->SetFilterIndex(selectedItem); UpdatePanelFileTypes(mOpenPanel, mFilePicker->GetFilterList()); NS_OBJC_END_TRY_IGNORE_BLOCK; } @end @implementation MOZSaveFilePickerPopUpObserver - (void)setPopUpButton:(NSPopUpButton*)aPopUpButton { mPopUpButton = aPopUpButton; } - (void)setSavePanel:(NSSavePanel*)aSavePanel { mSavePanel = aSavePanel; } - (void)setFilePicker:(nsFilePicker*)aFilePicker { mFilePicker = aFilePicker; } - (void)menuChangedItem:(NSNotification*)aSender { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; int32_t selectedItem = [mPopUpButton indexOfSelectedItem]; if (selectedItem < 0) { return; } mFilePicker->SetFilterIndex(selectedItem); NSArray* filterList = mFilePicker->GetFilterList(); if (filterList && [filterList count] > 0) { NSString* newExtension = [filterList objectAtIndex:0]; NSString* currentName = [mSavePanel nameFieldStringValue]; NSString* baseName = [currentName stringByDeletingPathExtension]; if (baseName.length > 0) { [mSavePanel setNameFieldStringValue: [baseName stringByAppendingPathExtension:newExtension]]; // Keep the panel's allowed type in sync with the name field so the new // extension is preserved on the saved file. mSavePanel.allowedFileTypes = @[ newExtension ]; } } // For the "All Files" filter GetFilterList returns nil; we leave the name // field and the allowed type untouched so the current extension is kept. NS_OBJC_END_TRY_IGNORE_BLOCK; } @end void nsFilePicker::PresentOpenPanel(bool aAllowMultiple, nsIFilePickerShownCallback* aCallback) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; MOZ_ASSERT(NS_IsMainThread()); mFiles.Clear(); NSOpenPanel* thePanel = [NSOpenPanel openPanel]; SetShowHiddenFileState(thePanel); // Set the options for how the get file dialog will appear SetDialogTitle(mTitle, thePanel); [thePanel setAllowsMultipleSelection:aAllowMultiple]; [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(); NSString* theDir = PanelDefaultDirectory(); // If no other dir was set and this is the "Choose application..." dialog // then use the Applications folder. if (!theDir && filters && [filters count] == 1 && [static_cast([filters objectAtIndex:0]) isEqualToString:@"app"]) { theDir = @"/Applications/"; } if (theDir) { [thePanel setDirectoryURL:[NSURL fileURLWithPath:theDir isDirectory:YES]]; } MOZFilePickerPopUpObserver* observer = nil; if (mFilters.Length() > 1) { observer = [[MOZFilePickerPopUpObserver alloc] init]; NSView* accessoryView = GetAccessoryView(); [thePanel setAccessoryView:accessoryView]; NSPopUpButton* popupButton = [accessoryView viewWithTag:kSaveTypeControlTag]; [observer setPopUpButton:popupButton]; [observer setOpenPanel:thePanel]; [observer setFilePicker:this]; [[NSNotificationCenter defaultCenter] addObserver:observer selector:@selector(menuChangedItem:) name:NSMenuWillSendActionNotification object:[popupButton menu]]; UpdatePanelFileTypes(thePanel, filters); } else { // If we show all file types, also "expose" bundles' contents. if (!filters) { [thePanel setTreatsFilePackagesAsDirectories:YES]; } [thePanel setAllowedFileTypes:filters]; } RefPtr self = this; nsCOMPtr callback = aCallback; BeginPanelAsync(thePanel, ^(NSModalResponse result) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; if (observer) { [[NSNotificationCenter defaultCenter] removeObserver:observer]; [observer release]; } ResultCode retVal = returnCancel; if (result != NSModalResponseCancel) { for (NSURL* url in [thePanel URLs]) { if (!url) { continue; } nsCOMPtr macLocalFile; if (NS_SUCCEEDED(NS_NewLocalFileWithCFURL( static_cast(url), getter_AddRefs(macLocalFile)))) { self->mFiles.AppendObject(macLocalFile); } } if (self->mFiles.Count() > 0) { retVal = returnOK; } } self->FinishOpenOrFolderPanel(callback, retVal); NS_OBJC_END_TRY_IGNORE_BLOCK; }); NS_OBJC_END_TRY_IGNORE_BLOCK; } void nsFilePicker::PresentFolderPanel(nsIFilePickerShownCallback* aCallback) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; MOZ_ASSERT(NS_IsMainThread()); mFiles.Clear(); 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]]; } RefPtr self = this; nsCOMPtr callback = aCallback; BeginPanelAsync(thePanel, ^(NSModalResponse result) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; ResultCode retVal = returnCancel; if (result != NSModalResponseCancel) { // get the path for the folder (we allow just 1, so that's all we get) NSArray* urls = [thePanel URLs]; if ([urls count] > 0) { NSURL* theURL = [urls objectAtIndex:0]; if (theURL) { nsCOMPtr macLocalFile; if (NS_SUCCEEDED( NS_NewLocalFileWithCFURL(static_cast(theURL), getter_AddRefs(macLocalFile)))) { self->mFiles.AppendObject(macLocalFile); retVal = returnOK; } } } } self->FinishOpenOrFolderPanel(callback, retVal); NS_OBJC_END_TRY_IGNORE_BLOCK; }); NS_OBJC_END_TRY_IGNORE_BLOCK; } void nsFilePicker::FinishOpenOrFolderPanel( nsIFilePickerShownCallback* aCallback, ResultCode aResult) { if (aResult == returnCancel || !ShouldRunContentAnalysis()) { InvokeFilePickerCallbackDeferred(aCallback, aResult); return; } // Content Analysis may show its own dialog, so as with // InvokeFilePickerCallbackDeferred, wait for the panel's modal session to // finish unwinding before starting the check. RefPtr self = this; nsCOMPtr callback = aCallback; NS_DispatchToMainThread(NS_NewRunnableFunction( "nsFilePicker::CheckContentAnalysis", [self, callback, aResult]() { nsCOMArray files; files.AppendElements(self->mFiles); self->CheckContentAnalysis(std::move(files)) ->Then( GetMainThreadSerialEventTarget(), __func__, [self, callback, aResult](nsCOMArray aAllowedFiles) { self->mFiles.Clear(); aAllowedFiles.SwapElements(self->mFiles); if (callback) { callback->Done(self->mFiles.IsEmpty() ? returnCancel : aResult); } }, [self, callback](nsresult aError) { self->mFiles.Clear(); if (callback) { callback->Done(returnCancel); } }); })); } void nsFilePicker::PresentSavePanel(nsIFilePickerShownCallback* aCallback) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; MOZ_ASSERT(NS_IsMainThread()); mFiles.Clear(); NSSavePanel* thePanel = [NSSavePanel savePanel]; SetShowHiddenFileState(thePanel); SetDialogTitle(mTitle, thePanel); // set up default file name NSString* defaultFilename = [NSString stringWithCharacters:reinterpret_cast( mDefaultFilename.get()) length:mDefaultFilename.Length()]; // set up accessory view for file format options MOZSaveFilePickerPopUpObserver* observer = nil; if (mFilters.Length()) { NSView* accessoryView = GetAccessoryView(); [thePanel setAccessoryView:accessoryView]; observer = [[MOZSaveFilePickerPopUpObserver alloc] init]; NSPopUpButton* popupButton = [accessoryView viewWithTag:kSaveTypeControlTag]; [observer setPopUpButton:popupButton]; [observer setSavePanel:thePanel]; [observer setFilePicker:this]; [[NSNotificationCenter defaultCenter] addObserver:observer selector:@selector(menuChangedItem:) name:NSMenuWillSendActionNotification object:[popupButton menu]]; } // Declare the default file's extension as the allowed type so that saving // without changing the format popup keeps it on the file. The observer // updates this when the user changes the format. NSString* defaultExtension = defaultFilename.pathExtension; if (defaultExtension.length != 0) { thePanel.allowedFileTypes = @[ defaultExtension ]; } // 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, static_cast(otherExtension), nullptr); 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]]; } [thePanel setNameFieldStringValue:defaultFilename]; RefPtr self = this; nsCOMPtr callback = aCallback; BeginPanelAsync(thePanel, ^(NSModalResponse result) { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; if (observer) { [[NSNotificationCenter defaultCenter] removeObserver:observer]; [observer release]; } ResultCode retVal = returnCancel; if (result != NSModalResponseCancel) { // get the save type NSPopUpButton* popupButton = [[thePanel accessoryView] viewWithTag:kSaveTypeControlTag]; if (popupButton) { self->mSelectedTypeIndex = [popupButton indexOfSelectedItem]; } NSURL* fileURL = [thePanel URL]; if (fileURL) { nsCOMPtr macLocalFile; if (NS_SUCCEEDED( NS_NewLocalFileWithCFURL(static_cast(fileURL), getter_AddRefs(macLocalFile)))) { self->mFiles.AppendObject(macLocalFile); // We tell if we are replacing by looking to see if the // file exists. The user will only have hit OK if they meant to // replace the file. if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) { retVal = returnReplace; } else { retVal = returnOK; } } } } InvokeFilePickerCallbackDeferred(callback, retVal); NS_OBJC_END_TRY_IGNORE_BLOCK; }); NS_OBJC_END_TRY_IGNORE_BLOCK; } NSArray* nsFilePicker::GetFilterList() { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; if (!mFilters.Length()) { return nil; } if (mFilters.Length() <= static_cast(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:reinterpret_cast( inTitle.get()) length:inTitle.Length()]]; if (!mOkButtonLabel.IsEmpty()) { [aPanel setPrompt:[NSString stringWithCharacters:reinterpret_cast( 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; } NS_IMETHODIMP nsFilePicker::GetDefaultExtension(nsAString& aExtension) { aExtension.Truncate(); return NS_OK; } NS_IMETHODIMP nsFilePicker::SetDefaultExtension(const nsAString& aExtension) { return NS_OK; } 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; } NS_IMETHODIMP nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) { *aFilterIndex = mSelectedTypeIndex; return NS_OK; } NS_IMETHODIMP nsFilePicker::SetFilterIndex(int32_t aFilterIndex) { mSelectedTypeIndex = aFilterIndex; return NS_OK; }