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