// Copyright (c) 2013 GitHub, Inc. // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. #include "atom/browser/ui/file_dialog.h" #import #import #include "atom/browser/native_window.h" #include "base/files/file_util.h" #include "base/mac/foundation_util.h" #include "base/mac/mac_util.h" #include "base/mac/scoped_cftyperef.h" #include "base/strings/sys_string_conversions.h" @interface PopUpButtonHandler : NSObject @property (nonatomic, strong) NSSavePanel *savePanel; @property (nonatomic, strong) NSArray *fileTypes; - (instancetype)initWithPanel:(NSSavePanel *)panel andTypes:(NSArray *)types; - (void)selectFormat:(id)sender; @end @implementation PopUpButtonHandler - (instancetype)initWithPanel:(NSSavePanel *)panel andTypes:(NSArray *)types { self = [super init]; if (self) { _savePanel = panel; _fileTypes = types; } return self; } - (void)selectFormat:(id)sender { NSPopUpButton *button = (NSPopUpButton *)sender; NSInteger selectedItemIndex = [button indexOfSelectedItem]; NSString *nameFieldString = [[self savePanel] nameFieldStringValue]; NSString *trimmedNameFieldString = [nameFieldString stringByDeletingPathExtension]; NSString *extension = [[self fileTypes] objectAtIndex: selectedItemIndex]; NSString *nameFieldStringWithExt = [NSString stringWithFormat:@"%@.%@", trimmedNameFieldString, extension]; [[self savePanel] setNameFieldStringValue:nameFieldStringWithExt]; [[self savePanel] setAllowedFileTypes:@[extension]]; } @end namespace file_dialog { namespace { static PopUpButtonHandler *popUpButtonHandler; void SetAllowedFileTypes(NSSavePanel* dialog, const Filters& filters) { NSMutableSet* file_type_set = [NSMutableSet set]; for (size_t i = 0; i < filters.size(); ++i) { const Filter& filter = filters[i]; for (size_t j = 0; j < filter.second.size(); ++j) { // If we meet a '*' file extension, we allow all the file types and no // need to set the specified file types. if (filter.second[j] == "*") { [dialog setAllowsOtherFileTypes:YES]; return; } base::ScopedCFTypeRef ext_cf( base::SysUTF8ToCFStringRef(filter.second[j])); [file_type_set addObject:base::mac::CFToNSCast(ext_cf.get())]; } } // Passing empty array to setAllowedFileTypes will cause exception. NSArray* file_types = nil; if ([file_type_set count]) file_types = [file_type_set allObjects]; [dialog setAllowedFileTypes:file_types]; if (!popUpButtonHandler) popUpButtonHandler = [[PopUpButtonHandler alloc] initWithPanel:dialog andTypes:file_types]; // add file format picker NSView *accessoryView = [[NSView alloc] initWithFrame:NSMakeRect(0.0, 0.0, 200, 32.0)]; NSTextField *label = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 60, 22)]; [label setEditable:NO]; [label setStringValue:@"Format:"]; [label setBordered:NO]; [label setBezeled:NO]; [label setDrawsBackground:NO]; NSPopUpButton *popupButton = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(50.0, 2, 140, 22.0) pullsDown:NO]; [popupButton addItemsWithTitles:file_types]; [popupButton setTarget:popUpButtonHandler]; [popupButton setAction:@selector(selectFormat:)]; [accessoryView addSubview:label]; [accessoryView addSubview:popupButton]; [dialog setAccessoryView:accessoryView]; } void SetupDialog(NSSavePanel* dialog, const DialogSettings& settings) { if (!settings.title.empty()) [dialog setTitle:base::SysUTF8ToNSString(settings.title)]; if (!settings.button_label.empty()) [dialog setPrompt:base::SysUTF8ToNSString(settings.button_label)]; if (!settings.message.empty()) [dialog setMessage:base::SysUTF8ToNSString(settings.message)]; if (!settings.name_field_label.empty()) [dialog setNameFieldLabel:base::SysUTF8ToNSString(settings.name_field_label)]; [dialog setShowsTagField:settings.shows_tag_field]; NSString* default_dir = nil; NSString* default_filename = nil; if (!settings.default_path.empty()) { if (base::DirectoryExists(settings.default_path)) { default_dir = base::SysUTF8ToNSString(settings.default_path.value()); } else { if (settings.default_path.IsAbsolute()) { default_dir = base::SysUTF8ToNSString(settings.default_path.DirName().value()); } default_filename = base::SysUTF8ToNSString(settings.default_path.BaseName().value()); } } if (settings.filters.empty()) { [dialog setAllowsOtherFileTypes:YES]; } else { // Set setAllowedFileTypes before setNameFieldStringValue as it might // override the extension set using setNameFieldStringValue SetAllowedFileTypes(dialog, settings.filters); } // Make sure the extension is always visible. Without this, the extension in // the default filename will not be used in the saved file. [dialog setExtensionHidden:NO]; if (default_dir) [dialog setDirectoryURL:[NSURL fileURLWithPath:default_dir]]; if (default_filename) [dialog setNameFieldStringValue:default_filename]; } void SetupDialogForProperties(NSOpenPanel* dialog, int properties) { [dialog setCanChooseFiles:(properties & FILE_DIALOG_OPEN_FILE)]; if (properties & FILE_DIALOG_OPEN_DIRECTORY) [dialog setCanChooseDirectories:YES]; if (properties & FILE_DIALOG_CREATE_DIRECTORY) [dialog setCanCreateDirectories:YES]; if (properties & FILE_DIALOG_MULTI_SELECTIONS) [dialog setAllowsMultipleSelection:YES]; if (properties & FILE_DIALOG_SHOW_HIDDEN_FILES) [dialog setShowsHiddenFiles:YES]; if (properties & FILE_DIALOG_NO_RESOLVE_ALIASES) [dialog setResolvesAliases:NO]; if (properties & FILE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY) [dialog setTreatsFilePackagesAsDirectories:YES]; } // Run modal dialog with parent window and return user's choice. int RunModalDialog(NSSavePanel* dialog, const DialogSettings& settings) { __block int chosen = NSFileHandlingPanelCancelButton; if (!settings.parent_window || !settings.parent_window->GetNativeWindow() || settings.force_detached) { chosen = [dialog runModal]; } else { NSWindow* window = settings.parent_window->GetNativeWindow(); [dialog beginSheetModalForWindow:window completionHandler:^(NSInteger c) { chosen = c; [NSApp stopModal]; }]; [NSApp runModalForWindow:window]; } return chosen; } // Create bookmark data and serialise it into a base64 string. std::string GetBookmarkDataFromNSURL(NSURL* url) { // Create the file if it doesn't exist (necessary for NSSavePanel options). NSFileManager *defaultManager = [NSFileManager defaultManager]; if (![defaultManager fileExistsAtPath: [url path]]) { [defaultManager createFileAtPath: [url path] contents: nil attributes: nil]; } NSError *error = nil; NSData *bookmarkData = [url bookmarkDataWithOptions: NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys: nil relativeToURL: nil error: &error]; if (error != nil) { // Send back an empty string if there was an error. return ""; } else { // Encode NSData in base64 then convert to NSString. NSString *base64data = [[NSString alloc] initWithData: [bookmarkData base64EncodedDataWithOptions: 0] encoding: NSUTF8StringEncoding]; return base::SysNSStringToUTF8(base64data); } } void ReadDialogPathsWithBookmarks(NSOpenPanel* dialog, std::vector* paths, std::vector* bookmarks) { NSArray* urls = [dialog URLs]; for (NSURL* url in urls) if ([url isFileURL]) { paths->push_back(base::FilePath(base::SysNSStringToUTF8([url path]))); bookmarks->push_back(GetBookmarkDataFromNSURL(url)); } } void ReadDialogPaths(NSOpenPanel* dialog, std::vector* paths) { std::vector ignored_bookmarks; ReadDialogPathsWithBookmarks(dialog, paths, &ignored_bookmarks); } } // namespace bool ShowOpenDialog(const DialogSettings& settings, std::vector* paths) { DCHECK(paths); NSOpenPanel* dialog = [NSOpenPanel openPanel]; SetupDialog(dialog, settings); SetupDialogForProperties(dialog, settings.properties); int chosen = RunModalDialog(dialog, settings); if (chosen == NSFileHandlingPanelCancelButton) return false; ReadDialogPaths(dialog, paths); return true; } void OpenDialogCompletion(int chosen, NSOpenPanel* dialog, const DialogSettings& settings, const OpenDialogCallback& callback) { if (chosen == NSFileHandlingPanelCancelButton) { #if defined(MAS_BUILD) callback.Run(false, std::vector(), std::vector()); #else callback.Run(false, std::vector()); #endif } else { std::vector paths; #if defined(MAS_BUILD) std::vector bookmarks; if (settings.security_scoped_bookmarks) { ReadDialogPathsWithBookmarks(dialog, &paths, &bookmarks); } else { ReadDialogPaths(dialog, &paths); } callback.Run(true, paths, bookmarks); #else ReadDialogPaths(dialog, &paths); callback.Run(true, paths); #endif } } void ShowOpenDialog(const DialogSettings& settings, const OpenDialogCallback& c) { NSOpenPanel* dialog = [NSOpenPanel openPanel]; SetupDialog(dialog, settings); SetupDialogForProperties(dialog, settings.properties); // Duplicate the callback object here since c is a reference and gcd would // only store the pointer, by duplication we can force gcd to store a copy. __block OpenDialogCallback callback = c; if (!settings.parent_window || !settings.parent_window->GetNativeWindow() || settings.force_detached) { int chosen = [dialog runModal]; OpenDialogCompletion(chosen, dialog, settings, callback); } else { NSWindow* window = settings.parent_window->GetNativeWindow(); [dialog beginSheetModalForWindow:window completionHandler:^(NSInteger chosen) { OpenDialogCompletion(chosen, dialog, settings, callback); }]; } } bool ShowSaveDialog(const DialogSettings& settings, base::FilePath* path) { DCHECK(path); NSSavePanel* dialog = [NSSavePanel savePanel]; SetupDialog(dialog, settings); int chosen = RunModalDialog(dialog, settings); if (chosen == NSFileHandlingPanelCancelButton || ![[dialog URL] isFileURL]) return false; *path = base::FilePath(base::SysNSStringToUTF8([[dialog URL] path])); return true; } void SaveDialogCompletion(int chosen, NSSavePanel* dialog, const DialogSettings& settings, const SaveDialogCallback& callback) { if (chosen == NSFileHandlingPanelCancelButton) { #if defined(MAS_BUILD) callback.Run(false, base::FilePath(), ""); #else callback.Run(false, base::FilePath()); #endif } else { std::string path = base::SysNSStringToUTF8([[dialog URL] path]); #if defined(MAS_BUILD) std::string bookmark; if (settings.security_scoped_bookmarks) { bookmark = GetBookmarkDataFromNSURL([dialog URL]); } callback.Run(true, base::FilePath(path), bookmark); #else callback.Run(true, base::FilePath(path)); #endif } } void ShowSaveDialog(const DialogSettings& settings, const SaveDialogCallback& c) { NSSavePanel* dialog = [NSSavePanel savePanel]; SetupDialog(dialog, settings); [dialog setCanSelectHiddenExtension:YES]; __block SaveDialogCallback callback = c; if (!settings.parent_window || !settings.parent_window->GetNativeWindow() || settings.force_detached) { int chosen = [dialog runModal]; SaveDialogCompletion(chosen, dialog, settings, callback); } else { NSWindow* window = settings.parent_window->GetNativeWindow(); [dialog beginSheetModalForWindow:window completionHandler:^(NSInteger chosen) { SaveDialogCompletion(chosen, dialog, settings, callback); }]; } } } // namespace file_dialog