1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/ui/shell_dialogs.h" 6 7 #include <windows.h> 8 #include <commdlg.h> 9 #include <shlobj.h> 10 11 #include <algorithm> 12 #include <set> 13 14 #include "base/file_path.h" 15 #include "base/file_util.h" 16 #include "base/message_loop.h" 17 #include "base/string_split.h" 18 #include "base/threading/thread.h" 19 #include "base/utf_string_conversions.h" 20 #include "base/win/registry.h" 21 #include "base/win/scoped_comptr.h" 22 #include "base/win/windows_version.h" 23 #include "content/browser/browser_thread.h" 24 #include "grit/app_strings.h" 25 #include "grit/generated_resources.h" 26 #include "ui/base/l10n/l10n_util.h" 27 28 // This function takes the output of a SaveAs dialog: a filename, a filter and 29 // the extension originally suggested to the user (shown in the dialog box) and 30 // returns back the filename with the appropriate extension tacked on. If the 31 // user requests an unknown extension and is not using the 'All files' filter, 32 // the suggested extension will be appended, otherwise we will leave the 33 // filename unmodified. |filename| should contain the filename selected in the 34 // SaveAs dialog box and may include the path, |filter_selected| should be 35 // '*.something', for example '*.*' or it can be blank (which is treated as 36 // *.*). |suggested_ext| should contain the extension without the dot (.) in 37 // front, for example 'jpg'. 38 std::wstring AppendExtensionIfNeeded(const std::wstring& filename, 39 const std::wstring& filter_selected, 40 const std::wstring& suggested_ext) { 41 DCHECK(!filename.empty()); 42 std::wstring return_value = filename; 43 44 // If we wanted a specific extension, but the user's filename deleted it or 45 // changed it to something that the system doesn't understand, re-append. 46 // Careful: Checking net::GetMimeTypeFromExtension() will only find 47 // extensions with a known MIME type, which many "known" extensions on Windows 48 // don't have. So we check directly for the "known extension" registry key. 49 std::wstring file_extension(file_util::GetFileExtensionFromPath(filename)); 50 std::wstring key(L"." + file_extension); 51 if (!(filter_selected.empty() || filter_selected == L"*.*") && 52 !base::win::RegKey(HKEY_CLASSES_ROOT, key.c_str(), KEY_READ).Valid() && 53 file_extension != suggested_ext) { 54 if (return_value[return_value.length() - 1] != L'.') 55 return_value.append(L"."); 56 return_value.append(suggested_ext); 57 } 58 59 // Strip any trailing dots, which Windows doesn't allow. 60 size_t index = return_value.find_last_not_of(L'.'); 61 if (index < return_value.size() - 1) 62 return_value.resize(index + 1); 63 64 return return_value; 65 } 66 67 namespace { 68 69 // Get the file type description from the registry. This will be "Text Document" 70 // for .txt files, "JPEG Image" for .jpg files, etc. If the registry doesn't 71 // have an entry for the file type, we return false, true if the description was 72 // found. 'file_ext' must be in form ".txt". 73 static bool GetRegistryDescriptionFromExtension(const std::wstring& file_ext, 74 std::wstring* reg_description) { 75 DCHECK(reg_description); 76 base::win::RegKey reg_ext(HKEY_CLASSES_ROOT, file_ext.c_str(), KEY_READ); 77 std::wstring reg_app; 78 if (reg_ext.ReadValue(NULL, ®_app) == ERROR_SUCCESS && !reg_app.empty()) { 79 base::win::RegKey reg_link(HKEY_CLASSES_ROOT, reg_app.c_str(), KEY_READ); 80 if (reg_link.ReadValue(NULL, reg_description) == ERROR_SUCCESS) 81 return true; 82 } 83 return false; 84 } 85 86 // Set up a filter for a Save/Open dialog, which will consist of |file_ext| file 87 // extensions (internally separated by semicolons), |ext_desc| as the text 88 // descriptions of the |file_ext| types (optional), and (optionally) the default 89 // 'All Files' view. The purpose of the filter is to show only files of a 90 // particular type in a Windows Save/Open dialog box. The resulting filter is 91 // returned. The filters created here are: 92 // 1. only files that have 'file_ext' as their extension 93 // 2. all files (only added if 'include_all_files' is true) 94 // Example: 95 // file_ext: { "*.txt", "*.htm;*.html" } 96 // ext_desc: { "Text Document" } 97 // returned: "Text Document\0*.txt\0HTML Document\0*.htm;*.html\0" 98 // "All Files\0*.*\0\0" (in one big string) 99 // If a description is not provided for a file extension, it will be retrieved 100 // from the registry. If the file extension does not exist in the registry, it 101 // will be omitted from the filter, as it is likely a bogus extension. 102 std::wstring FormatFilterForExtensions( 103 const std::vector<std::wstring>& file_ext, 104 const std::vector<std::wstring>& ext_desc, 105 bool include_all_files) { 106 const std::wstring all_ext = L"*.*"; 107 const std::wstring all_desc = 108 l10n_util::GetStringUTF16(IDS_APP_SAVEAS_ALL_FILES); 109 110 DCHECK(file_ext.size() >= ext_desc.size()); 111 112 std::wstring result; 113 114 for (size_t i = 0; i < file_ext.size(); ++i) { 115 std::wstring ext = file_ext[i]; 116 std::wstring desc; 117 if (i < ext_desc.size()) 118 desc = ext_desc[i]; 119 120 if (ext.empty()) { 121 // Force something reasonable to appear in the dialog box if there is no 122 // extension provided. 123 include_all_files = true; 124 continue; 125 } 126 127 if (desc.empty()) { 128 DCHECK(ext.find(L'.') != std::wstring::npos); 129 std::wstring first_extension = ext.substr(ext.find(L'.')); 130 size_t first_separator_index = first_extension.find(L';'); 131 if (first_separator_index != std::wstring::npos) 132 first_extension = first_extension.substr(0, first_separator_index); 133 134 // Find the extension name without the preceeding '.' character. 135 std::wstring ext_name = first_extension; 136 size_t ext_index = ext_name.find_first_not_of(L'.'); 137 if (ext_index != std::wstring::npos) 138 ext_name = ext_name.substr(ext_index); 139 140 if (!GetRegistryDescriptionFromExtension(first_extension, &desc)) { 141 // The extension doesn't exist in the registry. Create a description 142 // based on the unknown extension type (i.e. if the extension is .qqq, 143 // the we create a description "QQQ File (.qqq)"). 144 include_all_files = true; 145 desc = l10n_util::GetStringFUTF16(IDS_APP_SAVEAS_EXTENSION_FORMAT, 146 l10n_util::ToUpper(ext_name), 147 ext_name); 148 } 149 if (desc.empty()) 150 desc = L"*." + ext_name; 151 } 152 153 result.append(desc.c_str(), desc.size() + 1); // Append NULL too. 154 result.append(ext.c_str(), ext.size() + 1); 155 } 156 157 if (include_all_files) { 158 result.append(all_desc.c_str(), all_desc.size() + 1); 159 result.append(all_ext.c_str(), all_ext.size() + 1); 160 } 161 162 result.append(1, '\0'); // Double NULL required. 163 return result; 164 } 165 166 // Enforce visible dialog box. 167 UINT_PTR CALLBACK SaveAsDialogHook(HWND dialog, UINT message, 168 WPARAM wparam, LPARAM lparam) { 169 static const UINT kPrivateMessage = 0x2F3F; 170 switch (message) { 171 case WM_INITDIALOG: { 172 // Do nothing here. Just post a message to defer actual processing. 173 PostMessage(dialog, kPrivateMessage, 0, 0); 174 return TRUE; 175 } 176 case kPrivateMessage: { 177 // The dialog box is the parent of the current handle. 178 HWND real_dialog = GetParent(dialog); 179 180 // Retrieve the final size. 181 RECT dialog_rect; 182 GetWindowRect(real_dialog, &dialog_rect); 183 184 // Verify that the upper left corner is visible. 185 POINT point = { dialog_rect.left, dialog_rect.top }; 186 HMONITOR monitor1 = MonitorFromPoint(point, MONITOR_DEFAULTTONULL); 187 point.x = dialog_rect.right; 188 point.y = dialog_rect.bottom; 189 190 // Verify that the lower right corner is visible. 191 HMONITOR monitor2 = MonitorFromPoint(point, MONITOR_DEFAULTTONULL); 192 if (monitor1 && monitor2) 193 return 0; 194 195 // Some part of the dialog box is not visible, fix it by moving is to the 196 // client rect position of the browser window. 197 HWND parent_window = GetParent(real_dialog); 198 if (!parent_window) 199 return 0; 200 WINDOWINFO parent_info; 201 parent_info.cbSize = sizeof(WINDOWINFO); 202 GetWindowInfo(parent_window, &parent_info); 203 SetWindowPos(real_dialog, NULL, 204 parent_info.rcClient.left, 205 parent_info.rcClient.top, 206 0, 0, // Size. 207 SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE | 208 SWP_NOZORDER); 209 210 return 0; 211 } 212 } 213 return 0; 214 } 215 216 // Prompt the user for location to save a file. 217 // Callers should provide the filter string, and also a filter index. 218 // The parameter |index| indicates the initial index of filter description 219 // and filter pattern for the dialog box. If |index| is zero or greater than 220 // the number of total filter types, the system uses the first filter in the 221 // |filter| buffer. |index| is used to specify the initial selected extension, 222 // and when done contains the extension the user chose. The parameter 223 // |final_name| returns the file name which contains the drive designator, 224 // path, file name, and extension of the user selected file name. |def_ext| is 225 // the default extension to give to the file if the user did not enter an 226 // extension. If |ignore_suggested_ext| is true, any file extension contained in 227 // |suggested_name| will not be used to generate the file name. This is useful 228 // in the case of saving web pages, where we know the extension type already and 229 // where |suggested_name| may contain a '.' character as a valid part of the 230 // name, thus confusing our extension detection code. 231 bool SaveFileAsWithFilter(HWND owner, 232 const std::wstring& suggested_name, 233 const std::wstring& filter, 234 const std::wstring& def_ext, 235 bool ignore_suggested_ext, 236 unsigned* index, 237 std::wstring* final_name) { 238 DCHECK(final_name); 239 // Having an empty filter makes for a bad user experience. We should always 240 // specify a filter when saving. 241 DCHECK(!filter.empty()); 242 std::wstring file_part = FilePath(suggested_name).BaseName().value(); 243 244 // The size of the in/out buffer in number of characters we pass to win32 245 // GetSaveFileName. From MSDN "The buffer must be large enough to store the 246 // path and file name string or strings, including the terminating NULL 247 // character. ... The buffer should be at least 256 characters long.". 248 // _IsValidPathComDlg does a copy expecting at most MAX_PATH, otherwise will 249 // result in an error of FNERR_INVALIDFILENAME. So we should only pass the 250 // API a buffer of at most MAX_PATH. 251 wchar_t file_name[MAX_PATH]; 252 base::wcslcpy(file_name, file_part.c_str(), arraysize(file_name)); 253 254 OPENFILENAME save_as; 255 // We must do this otherwise the ofn's FlagsEx may be initialized to random 256 // junk in release builds which can cause the Places Bar not to show up! 257 ZeroMemory(&save_as, sizeof(save_as)); 258 save_as.lStructSize = sizeof(OPENFILENAME); 259 save_as.hwndOwner = owner; 260 save_as.hInstance = NULL; 261 262 save_as.lpstrFilter = filter.empty() ? NULL : filter.c_str(); 263 264 save_as.lpstrCustomFilter = NULL; 265 save_as.nMaxCustFilter = 0; 266 save_as.nFilterIndex = *index; 267 save_as.lpstrFile = file_name; 268 save_as.nMaxFile = arraysize(file_name); 269 save_as.lpstrFileTitle = NULL; 270 save_as.nMaxFileTitle = 0; 271 272 // Set up the initial directory for the dialog. 273 std::wstring directory; 274 if (!suggested_name.empty()) 275 directory = FilePath(suggested_name).DirName().value(); 276 277 save_as.lpstrInitialDir = directory.c_str(); 278 save_as.lpstrTitle = NULL; 279 save_as.Flags = OFN_OVERWRITEPROMPT | OFN_EXPLORER | OFN_ENABLESIZING | 280 OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST; 281 save_as.lpstrDefExt = &def_ext[0]; 282 save_as.lCustData = NULL; 283 284 if (base::win::GetVersion() < base::win::VERSION_VISTA) { 285 // The save as on Windows XP remembers its last position, 286 // and if the screen resolution changed, it will be off screen. 287 save_as.Flags |= OFN_ENABLEHOOK; 288 save_as.lpfnHook = &SaveAsDialogHook; 289 } 290 291 // Must be NULL or 0. 292 save_as.pvReserved = NULL; 293 save_as.dwReserved = 0; 294 295 if (!GetSaveFileName(&save_as)) { 296 // Zero means the dialog was closed, otherwise we had an error. 297 DWORD error_code = CommDlgExtendedError(); 298 if (error_code != 0) { 299 NOTREACHED() << "GetSaveFileName failed with code: " << error_code; 300 } 301 return false; 302 } 303 304 // Return the user's choice. 305 final_name->assign(save_as.lpstrFile); 306 *index = save_as.nFilterIndex; 307 308 // Figure out what filter got selected from the vector with embedded nulls. 309 // NOTE: The filter contains a string with embedded nulls, such as: 310 // JPG Image\0*.jpg\0All files\0*.*\0\0 311 // The filter index is 1-based index for which pair got selected. So, using 312 // the example above, if the first index was selected we need to skip 1 313 // instance of null to get to "*.jpg". 314 std::vector<std::wstring> filters; 315 if (!filter.empty() && save_as.nFilterIndex > 0) 316 base::SplitString(filter, '\0', &filters); 317 std::wstring filter_selected; 318 if (!filters.empty()) 319 filter_selected = filters[(2 * (save_as.nFilterIndex - 1)) + 1]; 320 321 // Get the extension that was suggested to the user (when the Save As dialog 322 // was opened). For saving web pages, we skip this step since there may be 323 // 'extension characters' in the title of the web page. 324 std::wstring suggested_ext; 325 if (!ignore_suggested_ext) 326 suggested_ext = file_util::GetFileExtensionFromPath(suggested_name); 327 328 // If we can't get the extension from the suggested_name, we use the default 329 // extension passed in. This is to cover cases like when saving a web page, 330 // where we get passed in a name without an extension and a default extension 331 // along with it. 332 if (suggested_ext.empty()) 333 suggested_ext = def_ext; 334 335 *final_name = 336 AppendExtensionIfNeeded(*final_name, filter_selected, suggested_ext); 337 return true; 338 } 339 340 // Prompt the user for location to save a file. 'suggested_name' is a full path 341 // that gives the dialog box a hint as to how to initialize itself. 342 // For example, a 'suggested_name' of: 343 // "C:\Documents and Settings\jojo\My Documents\picture.png" 344 // will start the dialog in the "C:\Documents and Settings\jojo\My Documents\" 345 // directory, and filter for .png file types. 346 // 'owner' is the window to which the dialog box is modal, NULL for a modeless 347 // dialog box. 348 // On success, returns true and 'final_name' contains the full path of the file 349 // that the user chose. On error, returns false, and 'final_name' is not 350 // modified. 351 bool SaveFileAs(HWND owner, 352 const std::wstring& suggested_name, 353 std::wstring* final_name) { 354 std::wstring file_ext = FilePath(suggested_name).Extension().insert(0, L"*"); 355 std::wstring filter = FormatFilterForExtensions( 356 std::vector<std::wstring>(1, file_ext), 357 std::vector<std::wstring>(), 358 true); 359 unsigned index = 1; 360 return SaveFileAsWithFilter(owner, 361 suggested_name, 362 filter, 363 L"", 364 false, 365 &index, 366 final_name); 367 } 368 369 } // namespace 370 371 // Helpers to show certain types of Windows shell dialogs in a way that doesn't 372 // block the UI of the entire app. 373 374 class ShellDialogThread : public base::Thread { 375 public: 376 ShellDialogThread() : base::Thread("Chrome_ShellDialogThread") { } 377 378 protected: 379 void Init() { 380 // Initializes the COM library on the current thread. 381 CoInitialize(NULL); 382 } 383 384 void CleanUp() { 385 // Closes the COM library on the current thread. CoInitialize must 386 // be balanced by a corresponding call to CoUninitialize. 387 CoUninitialize(); 388 } 389 390 private: 391 DISALLOW_COPY_AND_ASSIGN(ShellDialogThread); 392 }; 393 394 /////////////////////////////////////////////////////////////////////////////// 395 // A base class for all shell dialog implementations that handles showing a 396 // shell dialog modally on its own thread. 397 class BaseShellDialogImpl { 398 public: 399 BaseShellDialogImpl(); 400 virtual ~BaseShellDialogImpl(); 401 402 protected: 403 // Represents a run of a dialog. 404 struct RunState { 405 // Owning HWND, may be null. 406 HWND owner; 407 408 // Thread dialog is run on. 409 base::Thread* dialog_thread; 410 }; 411 412 // Called at the beginning of a modal dialog run. Disables the owner window 413 // and tracks it. Returns the message loop of the thread that the dialog will 414 // be run on. 415 RunState BeginRun(HWND owner); 416 417 // Cleans up after a dialog run. If the run_state has a valid HWND this makes 418 // sure that the window is enabled. This is essential because BeginRun 419 // aggressively guards against multiple modal dialogs per HWND. Must be called 420 // on the UI thread after the result of the dialog has been determined. 421 // 422 // In addition this deletes the Thread in RunState. 423 void EndRun(RunState run_state); 424 425 // Returns true if a modal shell dialog is currently active for the specified 426 // owner. Must be called on the UI thread. 427 bool IsRunningDialogForOwner(HWND owner) const; 428 429 // Disables the window |owner|. Can be run from either the ui or the dialog 430 // thread. Can be called on either the UI or the dialog thread. This function 431 // is called on the dialog thread after the modal Windows Common dialog 432 // functions return because Windows automatically re-enables the owning 433 // window when those functions return, but we don't actually want them to be 434 // re-enabled until the response of the dialog propagates back to the UI 435 // thread, so we disable the owner manually after the Common dialog function 436 // returns. 437 void DisableOwner(HWND owner); 438 439 private: 440 // Creates a thread to run a shell dialog on. Each dialog requires its own 441 // thread otherwise in some situations where a singleton owns a single 442 // instance of this object we can have a situation where a modal dialog in 443 // one window blocks the appearance of a modal dialog in another. 444 static base::Thread* CreateDialogThread(); 445 446 // Enables the window |owner_|. Can only be run from the ui thread. 447 void EnableOwner(HWND owner); 448 449 // A list of windows that currently own active shell dialogs for this 450 // instance. For example, if the DownloadManager owns an instance of this 451 // object and there are two browser windows open both with Save As dialog 452 // boxes active, this list will consist of the two browser windows' HWNDs. 453 // The derived class must call EndRun once the dialog is done showing to 454 // remove the owning HWND from this list. 455 // This object is static since it is maintained for all instances of this 456 // object - i.e. you can't have two file pickers open for the 457 // same owner, even though they might be represented by different instances 458 // of this object. 459 // This set only contains non-null HWNDs. NULL hwnds are not added to this 460 // list. 461 typedef std::set<HWND> Owners; 462 static Owners owners_; 463 static int instance_count_; 464 465 DISALLOW_COPY_AND_ASSIGN(BaseShellDialogImpl); 466 }; 467 468 // static 469 BaseShellDialogImpl::Owners BaseShellDialogImpl::owners_; 470 int BaseShellDialogImpl::instance_count_ = 0; 471 472 BaseShellDialogImpl::BaseShellDialogImpl() { 473 ++instance_count_; 474 } 475 476 BaseShellDialogImpl::~BaseShellDialogImpl() { 477 // All runs should be complete by the time this is called! 478 if (--instance_count_ == 0) 479 DCHECK(owners_.empty()); 480 } 481 482 BaseShellDialogImpl::RunState BaseShellDialogImpl::BeginRun(HWND owner) { 483 // Cannot run a modal shell dialog if one is already running for this owner. 484 DCHECK(!IsRunningDialogForOwner(owner)); 485 // The owner must be a top level window, otherwise we could end up with two 486 // entries in our map for the same top level window. 487 DCHECK(!owner || owner == GetAncestor(owner, GA_ROOT)); 488 RunState run_state; 489 run_state.dialog_thread = CreateDialogThread(); 490 run_state.owner = owner; 491 if (owner) { 492 owners_.insert(owner); 493 DisableOwner(owner); 494 } 495 return run_state; 496 } 497 498 void BaseShellDialogImpl::EndRun(RunState run_state) { 499 if (run_state.owner) { 500 DCHECK(IsRunningDialogForOwner(run_state.owner)); 501 EnableOwner(run_state.owner); 502 DCHECK(owners_.find(run_state.owner) != owners_.end()); 503 owners_.erase(run_state.owner); 504 } 505 DCHECK(run_state.dialog_thread); 506 delete run_state.dialog_thread; 507 } 508 509 bool BaseShellDialogImpl::IsRunningDialogForOwner(HWND owner) const { 510 return (owner && owners_.find(owner) != owners_.end()); 511 } 512 513 void BaseShellDialogImpl::DisableOwner(HWND owner) { 514 if (IsWindow(owner)) 515 EnableWindow(owner, FALSE); 516 } 517 518 // static 519 base::Thread* BaseShellDialogImpl::CreateDialogThread() { 520 base::Thread* thread = new ShellDialogThread; 521 bool started = thread->Start(); 522 DCHECK(started); 523 return thread; 524 } 525 526 void BaseShellDialogImpl::EnableOwner(HWND owner) { 527 if (IsWindow(owner)) 528 EnableWindow(owner, TRUE); 529 } 530 531 // Implementation of SelectFileDialog that shows a Windows common dialog for 532 // choosing a file or folder. 533 class SelectFileDialogImpl : public SelectFileDialog, 534 public BaseShellDialogImpl { 535 public: 536 explicit SelectFileDialogImpl(Listener* listener); 537 538 virtual bool IsRunning(HWND owning_hwnd) const; 539 virtual void ListenerDestroyed(); 540 541 protected: 542 // SelectFileDialog implementation: 543 virtual void SelectFileImpl(Type type, 544 const string16& title, 545 const FilePath& default_path, 546 const FileTypeInfo* file_types, 547 int file_type_index, 548 const FilePath::StringType& default_extension, 549 gfx::NativeWindow owning_window, 550 void* params); 551 552 private: 553 virtual ~SelectFileDialogImpl(); 554 555 // A struct for holding all the state necessary for displaying a Save dialog. 556 struct ExecuteSelectParams { 557 ExecuteSelectParams(Type type, 558 const std::wstring& title, 559 const FilePath& default_path, 560 const FileTypeInfo* file_types, 561 int file_type_index, 562 const std::wstring& default_extension, 563 RunState run_state, 564 HWND owner, 565 void* params) 566 : type(type), 567 title(title), 568 default_path(default_path), 569 file_type_index(file_type_index), 570 default_extension(default_extension), 571 run_state(run_state), 572 owner(owner), 573 params(params) { 574 if (file_types) { 575 this->file_types = *file_types; 576 } else { 577 this->file_types.include_all_files = true; 578 } 579 } 580 SelectFileDialog::Type type; 581 std::wstring title; 582 FilePath default_path; 583 FileTypeInfo file_types; 584 int file_type_index; 585 std::wstring default_extension; 586 RunState run_state; 587 HWND owner; 588 void* params; 589 }; 590 591 // Shows the file selection dialog modal to |owner| and calls the result 592 // back on the ui thread. Run on the dialog thread. 593 void ExecuteSelectFile(const ExecuteSelectParams& params); 594 595 // Notifies the listener that a folder was chosen. Run on the ui thread. 596 void FileSelected(const FilePath& path, int index, 597 void* params, RunState run_state); 598 599 // Notifies listener that multiple files were chosen. Run on the ui thread. 600 void MultiFilesSelected(const std::vector<FilePath>& paths, void* params, 601 RunState run_state); 602 603 // Notifies the listener that no file was chosen (the action was canceled). 604 // Run on the ui thread. 605 void FileNotSelected(void* params, RunState run_state); 606 607 // Runs a Folder selection dialog box, passes back the selected folder in 608 // |path| and returns true if the user clicks OK. If the user cancels the 609 // dialog box the value in |path| is not modified and returns false. |title| 610 // is the user-supplied title text to show for the dialog box. Run on the 611 // dialog thread. 612 bool RunSelectFolderDialog(const std::wstring& title, 613 HWND owner, 614 FilePath* path); 615 616 // Runs an Open file dialog box, with similar semantics for input paramaters 617 // as RunSelectFolderDialog. 618 bool RunOpenFileDialog(const std::wstring& title, 619 const std::wstring& filters, 620 HWND owner, 621 FilePath* path); 622 623 // Runs an Open file dialog box that supports multi-select, with similar 624 // semantics for input paramaters as RunOpenFileDialog. 625 bool RunOpenMultiFileDialog(const std::wstring& title, 626 const std::wstring& filter, 627 HWND owner, 628 std::vector<FilePath>* paths); 629 630 // The callback function for when the select folder dialog is opened. 631 static int CALLBACK BrowseCallbackProc(HWND window, UINT message, 632 LPARAM parameter, 633 LPARAM data); 634 635 636 DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl); 637 }; 638 639 SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener) 640 : SelectFileDialog(listener), 641 BaseShellDialogImpl() { 642 } 643 644 SelectFileDialogImpl::~SelectFileDialogImpl() { 645 } 646 647 void SelectFileDialogImpl::SelectFileImpl( 648 Type type, 649 const string16& title, 650 const FilePath& default_path, 651 const FileTypeInfo* file_types, 652 int file_type_index, 653 const FilePath::StringType& default_extension, 654 gfx::NativeWindow owning_window, 655 void* params) { 656 ExecuteSelectParams execute_params(type, UTF16ToWide(title), default_path, 657 file_types, file_type_index, 658 default_extension, BeginRun(owning_window), 659 owning_window, params); 660 execute_params.run_state.dialog_thread->message_loop()->PostTask(FROM_HERE, 661 NewRunnableMethod(this, &SelectFileDialogImpl::ExecuteSelectFile, 662 execute_params)); 663 } 664 665 bool SelectFileDialogImpl::IsRunning(HWND owning_hwnd) const { 666 return listener_ && IsRunningDialogForOwner(owning_hwnd); 667 } 668 669 void SelectFileDialogImpl::ListenerDestroyed() { 670 // Our associated listener has gone away, so we shouldn't call back to it if 671 // our worker thread returns after the listener is dead. 672 listener_ = NULL; 673 } 674 675 void SelectFileDialogImpl::ExecuteSelectFile( 676 const ExecuteSelectParams& params) { 677 std::vector<std::wstring> exts; 678 for (size_t i = 0; i < params.file_types.extensions.size(); ++i) { 679 const std::vector<std::wstring>& inner_exts = 680 params.file_types.extensions[i]; 681 std::wstring ext_string; 682 for (size_t j = 0; j < inner_exts.size(); ++j) { 683 if (!ext_string.empty()) 684 ext_string.push_back(L';'); 685 ext_string.append(L"*."); 686 ext_string.append(inner_exts[j]); 687 } 688 exts.push_back(ext_string); 689 } 690 std::wstring filter = FormatFilterForExtensions( 691 exts, 692 params.file_types.extension_description_overrides, 693 params.file_types.include_all_files); 694 695 FilePath path = params.default_path; 696 bool success = false; 697 unsigned filter_index = params.file_type_index; 698 if (params.type == SELECT_FOLDER) { 699 success = RunSelectFolderDialog(params.title, 700 params.run_state.owner, 701 &path); 702 } else if (params.type == SELECT_SAVEAS_FILE) { 703 std::wstring path_as_wstring = path.value(); 704 success = SaveFileAsWithFilter(params.run_state.owner, 705 params.default_path.value(), filter, 706 params.default_extension, false, &filter_index, &path_as_wstring); 707 if (success) 708 path = FilePath(path_as_wstring); 709 DisableOwner(params.run_state.owner); 710 } else if (params.type == SELECT_OPEN_FILE) { 711 success = RunOpenFileDialog(params.title, filter, 712 params.run_state.owner, &path); 713 } else if (params.type == SELECT_OPEN_MULTI_FILE) { 714 std::vector<FilePath> paths; 715 if (RunOpenMultiFileDialog(params.title, filter, 716 params.run_state.owner, &paths)) { 717 BrowserThread::PostTask( 718 BrowserThread::UI, FROM_HERE, 719 NewRunnableMethod( 720 this, &SelectFileDialogImpl::MultiFilesSelected, paths, 721 params.params, params.run_state)); 722 return; 723 } 724 } 725 726 if (success) { 727 BrowserThread::PostTask( 728 BrowserThread::UI, FROM_HERE, 729 NewRunnableMethod( 730 this, &SelectFileDialogImpl::FileSelected, path, filter_index, 731 params.params, params.run_state)); 732 } else { 733 BrowserThread::PostTask( 734 BrowserThread::UI, FROM_HERE, 735 NewRunnableMethod( 736 this, &SelectFileDialogImpl::FileNotSelected, params.params, 737 params.run_state)); 738 } 739 } 740 741 void SelectFileDialogImpl::FileSelected(const FilePath& selected_folder, 742 int index, 743 void* params, 744 RunState run_state) { 745 if (listener_) 746 listener_->FileSelected(selected_folder, index, params); 747 EndRun(run_state); 748 } 749 750 void SelectFileDialogImpl::MultiFilesSelected( 751 const std::vector<FilePath>& selected_files, 752 void* params, 753 RunState run_state) { 754 if (listener_) 755 listener_->MultiFilesSelected(selected_files, params); 756 EndRun(run_state); 757 } 758 759 void SelectFileDialogImpl::FileNotSelected(void* params, RunState run_state) { 760 if (listener_) 761 listener_->FileSelectionCanceled(params); 762 EndRun(run_state); 763 } 764 765 int CALLBACK SelectFileDialogImpl::BrowseCallbackProc(HWND window, 766 UINT message, 767 LPARAM parameter, 768 LPARAM data) { 769 if (message == BFFM_INITIALIZED) { 770 // WParam is TRUE since passing a path. 771 // data lParam member of the BROWSEINFO structure. 772 SendMessage(window, BFFM_SETSELECTION, TRUE, (LPARAM)data); 773 } 774 return 0; 775 } 776 777 bool SelectFileDialogImpl::RunSelectFolderDialog(const std::wstring& title, 778 HWND owner, 779 FilePath* path) { 780 DCHECK(path); 781 782 wchar_t dir_buffer[MAX_PATH + 1]; 783 784 bool result = false; 785 BROWSEINFO browse_info = {0}; 786 browse_info.hwndOwner = owner; 787 browse_info.lpszTitle = title.c_str(); 788 browse_info.pszDisplayName = dir_buffer; 789 browse_info.ulFlags = BIF_USENEWUI | BIF_RETURNONLYFSDIRS; 790 791 if (path->value().length()) { 792 // Highlight the current value. 793 browse_info.lParam = (LPARAM)path->value().c_str(); 794 browse_info.lpfn = &BrowseCallbackProc; 795 } 796 797 LPITEMIDLIST list = SHBrowseForFolder(&browse_info); 798 DisableOwner(owner); 799 if (list) { 800 STRRET out_dir_buffer; 801 ZeroMemory(&out_dir_buffer, sizeof(out_dir_buffer)); 802 out_dir_buffer.uType = STRRET_WSTR; 803 base::win::ScopedComPtr<IShellFolder> shell_folder; 804 if (SHGetDesktopFolder(shell_folder.Receive()) == NOERROR) { 805 HRESULT hr = shell_folder->GetDisplayNameOf(list, SHGDN_FORPARSING, 806 &out_dir_buffer); 807 if (SUCCEEDED(hr) && out_dir_buffer.uType == STRRET_WSTR) { 808 *path = FilePath(out_dir_buffer.pOleStr); 809 CoTaskMemFree(out_dir_buffer.pOleStr); 810 result = true; 811 } else { 812 // Use old way if we don't get what we want. 813 wchar_t old_out_dir_buffer[MAX_PATH + 1]; 814 if (SHGetPathFromIDList(list, old_out_dir_buffer)) { 815 *path = FilePath(old_out_dir_buffer); 816 result = true; 817 } 818 } 819 820 // According to MSDN, win2000 will not resolve shortcuts, so we do it 821 // ourself. 822 file_util::ResolveShortcut(path); 823 } 824 CoTaskMemFree(list); 825 } 826 return result; 827 } 828 829 bool SelectFileDialogImpl::RunOpenFileDialog( 830 const std::wstring& title, 831 const std::wstring& filter, 832 HWND owner, 833 FilePath* path) { 834 OPENFILENAME ofn; 835 // We must do this otherwise the ofn's FlagsEx may be initialized to random 836 // junk in release builds which can cause the Places Bar not to show up! 837 ZeroMemory(&ofn, sizeof(ofn)); 838 ofn.lStructSize = sizeof(ofn); 839 ofn.hwndOwner = owner; 840 841 wchar_t filename[MAX_PATH]; 842 // According to http://support.microsoft.com/?scid=kb;en-us;222003&x=8&y=12, 843 // The lpstrFile Buffer MUST be NULL Terminated. 844 filename[0] = 0; 845 // Define the dir in here to keep the string buffer pointer pointed to 846 // ofn.lpstrInitialDir available during the period of running the 847 // GetOpenFileName. 848 FilePath dir; 849 // Use lpstrInitialDir to specify the initial directory 850 if (!path->empty()) { 851 bool is_dir; 852 base::PlatformFileInfo file_info; 853 if (file_util::GetFileInfo(*path, &file_info)) 854 is_dir = file_info.is_directory; 855 else 856 is_dir = file_util::EndsWithSeparator(*path); 857 if (is_dir) { 858 ofn.lpstrInitialDir = path->value().c_str(); 859 } else { 860 dir = path->DirName(); 861 ofn.lpstrInitialDir = dir.value().c_str(); 862 // Only pure filename can be put in lpstrFile field. 863 base::wcslcpy(filename, path->BaseName().value().c_str(), 864 arraysize(filename)); 865 } 866 } 867 868 ofn.lpstrFile = filename; 869 ofn.nMaxFile = MAX_PATH; 870 871 // We use OFN_NOCHANGEDIR so that the user can rename or delete the directory 872 // without having to close Chrome first. 873 ofn.Flags = OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; 874 875 if (!filter.empty()) 876 ofn.lpstrFilter = filter.c_str(); 877 bool success = !!GetOpenFileName(&ofn); 878 DisableOwner(owner); 879 if (success) 880 *path = FilePath(filename); 881 return success; 882 } 883 884 bool SelectFileDialogImpl::RunOpenMultiFileDialog( 885 const std::wstring& title, 886 const std::wstring& filter, 887 HWND owner, 888 std::vector<FilePath>* paths) { 889 OPENFILENAME ofn; 890 // We must do this otherwise the ofn's FlagsEx may be initialized to random 891 // junk in release builds which can cause the Places Bar not to show up! 892 ZeroMemory(&ofn, sizeof(ofn)); 893 ofn.lStructSize = sizeof(ofn); 894 ofn.hwndOwner = owner; 895 896 scoped_array<wchar_t> filename(new wchar_t[UNICODE_STRING_MAX_CHARS]); 897 filename[0] = 0; 898 899 ofn.lpstrFile = filename.get(); 900 ofn.nMaxFile = UNICODE_STRING_MAX_CHARS; 901 // We use OFN_NOCHANGEDIR so that the user can rename or delete the directory 902 // without having to close Chrome first. 903 ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER 904 | OFN_HIDEREADONLY | OFN_ALLOWMULTISELECT; 905 906 if (!filter.empty()) { 907 ofn.lpstrFilter = filter.c_str(); 908 } 909 bool success = !!GetOpenFileName(&ofn); 910 DisableOwner(owner); 911 if (success) { 912 std::vector<FilePath> files; 913 const wchar_t* selection = ofn.lpstrFile; 914 while (*selection) { // Empty string indicates end of list. 915 files.push_back(FilePath(selection)); 916 // Skip over filename and null-terminator. 917 selection += files.back().value().length() + 1; 918 } 919 if (files.empty()) { 920 success = false; 921 } else if (files.size() == 1) { 922 // When there is one file, it contains the path and filename. 923 paths->swap(files); 924 } else { 925 // Otherwise, the first string is the path, and the remainder are 926 // filenames. 927 std::vector<FilePath>::iterator path = files.begin(); 928 for (std::vector<FilePath>::iterator file = path + 1; 929 file != files.end(); ++file) { 930 paths->push_back(path->Append(*file)); 931 } 932 } 933 } 934 return success; 935 } 936 937 // static 938 SelectFileDialog* SelectFileDialog::Create(Listener* listener) { 939 return new SelectFileDialogImpl(listener); 940 } 941