file_dialog_mac.mm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. // Copyright (c) 2013 GitHub, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "atom/browser/ui/file_dialog.h"
  5. #import <Cocoa/Cocoa.h>
  6. #import <CoreServices/CoreServices.h>
  7. #include "atom/browser/native_window.h"
  8. #include "base/files/file_util.h"
  9. #include "base/mac/foundation_util.h"
  10. #include "base/mac/mac_util.h"
  11. #include "base/mac/scoped_cftyperef.h"
  12. #include "base/strings/sys_string_conversions.h"
  13. @interface PopUpButtonHandler : NSObject
  14. @property(nonatomic, assign) NSSavePanel* savePanel;
  15. @property(nonatomic, strong) NSArray* fileTypesList;
  16. - (instancetype)initWithPanel:(NSSavePanel*)panel
  17. andTypesList:(NSArray*)typesList;
  18. - (void)selectFormat:(id)sender;
  19. @end
  20. @implementation PopUpButtonHandler
  21. @synthesize savePanel;
  22. @synthesize fileTypesList;
  23. - (instancetype)initWithPanel:(NSSavePanel*)panel
  24. andTypesList:(NSArray*)typesList {
  25. self = [super init];
  26. if (self) {
  27. [self setSavePanel:panel];
  28. [self setFileTypesList:typesList];
  29. }
  30. return self;
  31. }
  32. - (void)selectFormat:(id)sender {
  33. NSPopUpButton* button = (NSPopUpButton*)sender;
  34. NSInteger selectedItemIndex = [button indexOfSelectedItem];
  35. NSArray* list = [self fileTypesList];
  36. NSArray* fileTypes = [list objectAtIndex:selectedItemIndex];
  37. // If we meet a '*' file extension, we allow all the file types and no
  38. // need to set the specified file types.
  39. if ([fileTypes count] == 0 || [fileTypes containsObject:@"*"])
  40. [[self savePanel] setAllowedFileTypes:nil];
  41. else
  42. [[self savePanel] setAllowedFileTypes:fileTypes];
  43. }
  44. @end
  45. // Manages the PopUpButtonHandler.
  46. @interface AtomAccessoryView : NSView
  47. @end
  48. @implementation AtomAccessoryView
  49. - (void)dealloc {
  50. auto* popupButton =
  51. static_cast<NSPopUpButton*>([[self subviews] objectAtIndex:1]);
  52. [[popupButton target] release];
  53. [super dealloc];
  54. }
  55. @end
  56. namespace file_dialog {
  57. DialogSettings::DialogSettings() = default;
  58. DialogSettings::~DialogSettings() = default;
  59. namespace {
  60. void SetAllowedFileTypes(NSSavePanel* dialog, const Filters& filters) {
  61. NSMutableArray* file_types_list = [NSMutableArray array];
  62. NSMutableArray* filter_names = [NSMutableArray array];
  63. // Create array to keep file types and their name.
  64. for (const Filter& filter : filters) {
  65. NSMutableSet* file_type_set = [NSMutableSet set];
  66. base::ScopedCFTypeRef<CFStringRef> name_cf(
  67. base::SysUTF8ToCFStringRef(filter.first));
  68. [filter_names addObject:base::mac::CFToNSCast(name_cf.get())];
  69. for (const std::string& ext : filter.second) {
  70. base::ScopedCFTypeRef<CFStringRef> ext_cf(
  71. base::SysUTF8ToCFStringRef(ext));
  72. [file_type_set addObject:base::mac::CFToNSCast(ext_cf.get())];
  73. }
  74. [file_types_list addObject:[file_type_set allObjects]];
  75. }
  76. // Passing empty array to setAllowedFileTypes will cause exception.
  77. NSArray* file_types = nil;
  78. NSUInteger count = [file_types_list count];
  79. if (count > 0) {
  80. file_types = [[file_types_list objectAtIndex:0] allObjects];
  81. // If we meet a '*' file extension, we allow all the file types and no
  82. // need to set the specified file types.
  83. if ([file_types count] == 0 || [file_types containsObject:@"*"])
  84. file_types = nil;
  85. }
  86. [dialog setAllowedFileTypes:file_types];
  87. if (count <= 1)
  88. return; // don't add file format picker
  89. // Add file format picker.
  90. AtomAccessoryView* accessoryView =
  91. [[AtomAccessoryView alloc] initWithFrame:NSMakeRect(0.0, 0.0, 200, 32.0)];
  92. NSTextField* label =
  93. [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 60, 22)];
  94. [label setEditable:NO];
  95. [label setStringValue:@"Format:"];
  96. [label setBordered:NO];
  97. [label setBezeled:NO];
  98. [label setDrawsBackground:NO];
  99. NSPopUpButton* popupButton =
  100. [[NSPopUpButton alloc] initWithFrame:NSMakeRect(50.0, 2, 140, 22.0)
  101. pullsDown:NO];
  102. PopUpButtonHandler* popUpButtonHandler =
  103. [[PopUpButtonHandler alloc] initWithPanel:dialog
  104. andTypesList:file_types_list];
  105. [popupButton addItemsWithTitles:filter_names];
  106. [popupButton setTarget:popUpButtonHandler];
  107. [popupButton setAction:@selector(selectFormat:)];
  108. [accessoryView addSubview:[label autorelease]];
  109. [accessoryView addSubview:[popupButton autorelease]];
  110. [dialog setAccessoryView:[accessoryView autorelease]];
  111. }
  112. void SetupDialog(NSSavePanel* dialog, const DialogSettings& settings) {
  113. if (!settings.title.empty())
  114. [dialog setTitle:base::SysUTF8ToNSString(settings.title)];
  115. if (!settings.button_label.empty())
  116. [dialog setPrompt:base::SysUTF8ToNSString(settings.button_label)];
  117. if (!settings.message.empty())
  118. [dialog setMessage:base::SysUTF8ToNSString(settings.message)];
  119. if (!settings.name_field_label.empty())
  120. [dialog
  121. setNameFieldLabel:base::SysUTF8ToNSString(settings.name_field_label)];
  122. [dialog setShowsTagField:settings.shows_tag_field];
  123. NSString* default_dir = nil;
  124. NSString* default_filename = nil;
  125. if (!settings.default_path.empty()) {
  126. base::ThreadRestrictions::ScopedAllowIO allow_io;
  127. if (base::DirectoryExists(settings.default_path)) {
  128. default_dir = base::SysUTF8ToNSString(settings.default_path.value());
  129. } else {
  130. if (settings.default_path.IsAbsolute()) {
  131. default_dir =
  132. base::SysUTF8ToNSString(settings.default_path.DirName().value());
  133. }
  134. default_filename =
  135. base::SysUTF8ToNSString(settings.default_path.BaseName().value());
  136. }
  137. }
  138. if (settings.filters.empty()) {
  139. [dialog setAllowsOtherFileTypes:YES];
  140. } else {
  141. // Set setAllowedFileTypes before setNameFieldStringValue as it might
  142. // override the extension set using setNameFieldStringValue
  143. SetAllowedFileTypes(dialog, settings.filters);
  144. }
  145. // Make sure the extension is always visible. Without this, the extension in
  146. // the default filename will not be used in the saved file.
  147. [dialog setExtensionHidden:NO];
  148. if (default_dir)
  149. [dialog setDirectoryURL:[NSURL fileURLWithPath:default_dir]];
  150. if (default_filename)
  151. [dialog setNameFieldStringValue:default_filename];
  152. }
  153. void SetupDialogForProperties(NSOpenPanel* dialog, int properties) {
  154. [dialog setCanChooseFiles:(properties & FILE_DIALOG_OPEN_FILE)];
  155. if (properties & FILE_DIALOG_OPEN_DIRECTORY)
  156. [dialog setCanChooseDirectories:YES];
  157. if (properties & FILE_DIALOG_CREATE_DIRECTORY)
  158. [dialog setCanCreateDirectories:YES];
  159. if (properties & FILE_DIALOG_MULTI_SELECTIONS)
  160. [dialog setAllowsMultipleSelection:YES];
  161. if (properties & FILE_DIALOG_SHOW_HIDDEN_FILES)
  162. [dialog setShowsHiddenFiles:YES];
  163. if (properties & FILE_DIALOG_NO_RESOLVE_ALIASES)
  164. [dialog setResolvesAliases:NO];
  165. if (properties & FILE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY)
  166. [dialog setTreatsFilePackagesAsDirectories:YES];
  167. }
  168. // Run modal dialog with parent window and return user's choice.
  169. int RunModalDialog(NSSavePanel* dialog, const DialogSettings& settings) {
  170. __block int chosen = NSFileHandlingPanelCancelButton;
  171. if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
  172. settings.force_detached) {
  173. chosen = [dialog runModal];
  174. } else {
  175. NSWindow* window = settings.parent_window->GetNativeWindow();
  176. [dialog beginSheetModalForWindow:window
  177. completionHandler:^(NSInteger c) {
  178. chosen = c;
  179. [NSApp stopModal];
  180. }];
  181. [NSApp runModalForWindow:window];
  182. }
  183. return chosen;
  184. }
  185. // Create bookmark data and serialise it into a base64 string.
  186. std::string GetBookmarkDataFromNSURL(NSURL* url) {
  187. // Create the file if it doesn't exist (necessary for NSSavePanel options).
  188. NSFileManager* defaultManager = [NSFileManager defaultManager];
  189. if (![defaultManager fileExistsAtPath:[url path]]) {
  190. [defaultManager createFileAtPath:[url path] contents:nil attributes:nil];
  191. }
  192. NSError* error = nil;
  193. NSData* bookmarkData =
  194. [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
  195. includingResourceValuesForKeys:nil
  196. relativeToURL:nil
  197. error:&error];
  198. if (error != nil) {
  199. // Send back an empty string if there was an error.
  200. return "";
  201. } else {
  202. // Encode NSData in base64 then convert to NSString.
  203. NSString* base64data = [[NSString alloc]
  204. initWithData:[bookmarkData base64EncodedDataWithOptions:0]
  205. encoding:NSUTF8StringEncoding];
  206. return base::SysNSStringToUTF8(base64data);
  207. }
  208. }
  209. void ReadDialogPathsWithBookmarks(NSOpenPanel* dialog,
  210. std::vector<base::FilePath>* paths,
  211. std::vector<std::string>* bookmarks) {
  212. NSArray* urls = [dialog URLs];
  213. for (NSURL* url in urls)
  214. if ([url isFileURL]) {
  215. paths->push_back(base::FilePath(base::SysNSStringToUTF8([url path])));
  216. bookmarks->push_back(GetBookmarkDataFromNSURL(url));
  217. }
  218. }
  219. void ReadDialogPaths(NSOpenPanel* dialog, std::vector<base::FilePath>* paths) {
  220. std::vector<std::string> ignored_bookmarks;
  221. ReadDialogPathsWithBookmarks(dialog, paths, &ignored_bookmarks);
  222. }
  223. } // namespace
  224. bool ShowOpenDialog(const DialogSettings& settings,
  225. std::vector<base::FilePath>* paths) {
  226. DCHECK(paths);
  227. NSOpenPanel* dialog = [NSOpenPanel openPanel];
  228. SetupDialog(dialog, settings);
  229. SetupDialogForProperties(dialog, settings.properties);
  230. int chosen = RunModalDialog(dialog, settings);
  231. if (chosen == NSFileHandlingPanelCancelButton)
  232. return false;
  233. ReadDialogPaths(dialog, paths);
  234. return true;
  235. }
  236. void OpenDialogCompletion(int chosen,
  237. NSOpenPanel* dialog,
  238. const DialogSettings& settings,
  239. const OpenDialogCallback& callback) {
  240. if (chosen == NSFileHandlingPanelCancelButton) {
  241. #if defined(MAS_BUILD)
  242. callback.Run(false, std::vector<base::FilePath>(),
  243. std::vector<std::string>());
  244. #else
  245. callback.Run(false, std::vector<base::FilePath>());
  246. #endif
  247. } else {
  248. std::vector<base::FilePath> paths;
  249. #if defined(MAS_BUILD)
  250. std::vector<std::string> bookmarks;
  251. if (settings.security_scoped_bookmarks) {
  252. ReadDialogPathsWithBookmarks(dialog, &paths, &bookmarks);
  253. } else {
  254. ReadDialogPaths(dialog, &paths);
  255. }
  256. callback.Run(true, paths, bookmarks);
  257. #else
  258. ReadDialogPaths(dialog, &paths);
  259. callback.Run(true, paths);
  260. #endif
  261. }
  262. }
  263. void ShowOpenDialog(const DialogSettings& settings,
  264. const OpenDialogCallback& c) {
  265. NSOpenPanel* dialog = [NSOpenPanel openPanel];
  266. SetupDialog(dialog, settings);
  267. SetupDialogForProperties(dialog, settings.properties);
  268. // Duplicate the callback object here since c is a reference and gcd would
  269. // only store the pointer, by duplication we can force gcd to store a copy.
  270. __block OpenDialogCallback callback = c;
  271. if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
  272. settings.force_detached) {
  273. [dialog beginWithCompletionHandler:^(NSInteger chosen) {
  274. OpenDialogCompletion(chosen, dialog, settings, callback);
  275. }];
  276. } else {
  277. NSWindow* window = settings.parent_window->GetNativeWindow();
  278. [dialog beginSheetModalForWindow:window
  279. completionHandler:^(NSInteger chosen) {
  280. OpenDialogCompletion(chosen, dialog, settings, callback);
  281. }];
  282. }
  283. }
  284. bool ShowSaveDialog(const DialogSettings& settings, base::FilePath* path) {
  285. DCHECK(path);
  286. NSSavePanel* dialog = [NSSavePanel savePanel];
  287. SetupDialog(dialog, settings);
  288. int chosen = RunModalDialog(dialog, settings);
  289. if (chosen == NSFileHandlingPanelCancelButton || ![[dialog URL] isFileURL])
  290. return false;
  291. *path = base::FilePath(base::SysNSStringToUTF8([[dialog URL] path]));
  292. return true;
  293. }
  294. void SaveDialogCompletion(int chosen,
  295. NSSavePanel* dialog,
  296. const DialogSettings& settings,
  297. const SaveDialogCallback& callback) {
  298. if (chosen == NSFileHandlingPanelCancelButton) {
  299. #if defined(MAS_BUILD)
  300. callback.Run(false, base::FilePath(), "");
  301. #else
  302. callback.Run(false, base::FilePath());
  303. #endif
  304. } else {
  305. std::string path = base::SysNSStringToUTF8([[dialog URL] path]);
  306. #if defined(MAS_BUILD)
  307. std::string bookmark;
  308. if (settings.security_scoped_bookmarks) {
  309. bookmark = GetBookmarkDataFromNSURL([dialog URL]);
  310. }
  311. callback.Run(true, base::FilePath(path), bookmark);
  312. #else
  313. callback.Run(true, base::FilePath(path));
  314. #endif
  315. }
  316. }
  317. void ShowSaveDialog(const DialogSettings& settings,
  318. const SaveDialogCallback& c) {
  319. NSSavePanel* dialog = [NSSavePanel savePanel];
  320. SetupDialog(dialog, settings);
  321. [dialog setCanSelectHiddenExtension:YES];
  322. __block SaveDialogCallback callback = c;
  323. if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
  324. settings.force_detached) {
  325. [dialog beginWithCompletionHandler:^(NSInteger chosen) {
  326. SaveDialogCompletion(chosen, dialog, settings, callback);
  327. }];
  328. } else {
  329. NSWindow* window = settings.parent_window->GetNativeWindow();
  330. [dialog beginSheetModalForWindow:window
  331. completionHandler:^(NSInteger chosen) {
  332. SaveDialogCompletion(chosen, dialog, settings, callback);
  333. }];
  334. }
  335. }
  336. } // namespace file_dialog