OLD | NEW |
1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/shell_dialogs.h" | 5 #include "chrome/browser/shell_dialogs.h" |
6 | 6 |
7 #include <windows.h> | 7 #include <windows.h> |
8 #include <commdlg.h> | 8 #include <commdlg.h> |
9 #include <shlobj.h> | 9 #include <shlobj.h> |
10 | 10 |
11 #include <algorithm> | 11 #include <algorithm> |
12 #include <set> | 12 #include <set> |
13 | 13 |
14 #include "app/gfx/font.h" | 14 #include "app/gfx/font.h" |
15 #include "app/l10n_util.h" | 15 #include "app/l10n_util.h" |
16 #include "app/win_util.h" | 16 #include "app/win_util.h" |
17 #include "base/file_util.h" | 17 #include "base/file_util.h" |
18 #include "base/registry.h" | 18 #include "base/registry.h" |
19 #include "base/scoped_comptr_win.h" | 19 #include "base/scoped_comptr_win.h" |
20 #include "base/thread.h" | 20 #include "base/thread.h" |
21 #include "base/utf_string_conversions.h" | 21 #include "base/utf_string_conversions.h" |
| 22 #include "base/win_util.h" |
22 #include "chrome/browser/chrome_thread.h" | 23 #include "chrome/browser/chrome_thread.h" |
| 24 #include "grit/app_strings.h" |
23 #include "grit/generated_resources.h" | 25 #include "grit/generated_resources.h" |
| 26 #include "net/base/mime_util.h" |
| 27 |
| 28 namespace { |
| 29 |
| 30 // This function takes the output of a SaveAs dialog: a filename, a filter and |
| 31 // the extension originally suggested to the user (shown in the dialog box) and |
| 32 // returns back the filename with the appropriate extension tacked on. For |
| 33 // example, if you pass in 'foo' as filename with filter '*.jpg' this function |
| 34 // will return 'foo.jpg'. It respects MIME types, so if you pass in 'foo.jpeg' |
| 35 // with filer '*.jpg' it will return 'foo.jpeg' (will not append .jpg). |
| 36 // |filename| should contain the filename selected in the SaveAs dialog box and |
| 37 // may include the path, |filter_selected| should be '*.something', for example |
| 38 // '*.*' or it can be blank (which is treated as *.*). |suggested_ext| should |
| 39 // contain the extension without the dot (.) in front, for example 'jpg'. |
| 40 std::wstring AppendExtensionIfNeeded(const std::wstring& filename, |
| 41 const std::wstring& filter_selected, |
| 42 const std::wstring& suggested_ext) { |
| 43 std::wstring return_value = filename; |
| 44 |
| 45 // Get the extension the user ended up selecting. |
| 46 std::wstring selected_ext = file_util::GetFileExtensionFromPath(filename); |
| 47 |
| 48 if (filter_selected.empty() || filter_selected == L"*.*") { |
| 49 // If the user selects 'All files' we respect any extension given to us from |
| 50 // the File Save dialog. We also strip any trailing dots, which matches |
| 51 // Windows Explorer and is needed because Windows doesn't allow filenames |
| 52 // to have trailing dots. The GetSaveFileName dialog will not return a |
| 53 // string with only one or more dots. |
| 54 size_t index = return_value.find_last_not_of(L'.'); |
| 55 if (index < return_value.size() - 1) |
| 56 return_value.resize(index + 1); |
| 57 } else { |
| 58 // User selected a specific filter (not *.*) so we need to check if the |
| 59 // extension provided has the same mime type. If it doesn't we append the |
| 60 // extension. |
| 61 std::string suggested_mime_type, selected_mime_type; |
| 62 if (suggested_ext != selected_ext && |
| 63 (!net::GetMimeTypeFromExtension(suggested_ext, &suggested_mime_type) || |
| 64 !net::GetMimeTypeFromExtension(selected_ext, &selected_mime_type) || |
| 65 suggested_mime_type != selected_mime_type)) { |
| 66 return_value.append(L"."); |
| 67 return_value.append(suggested_ext); |
| 68 } |
| 69 } |
| 70 |
| 71 return return_value; |
| 72 } |
| 73 |
| 74 // Get the file type description from the registry. This will be "Text Document" |
| 75 // for .txt files, "JPEG Image" for .jpg files, etc. If the registry doesn't |
| 76 // have an entry for the file type, we return false, true if the description was |
| 77 // found. 'file_ext' must be in form ".txt". |
| 78 static bool GetRegistryDescriptionFromExtension(const std::wstring& file_ext, |
| 79 std::wstring* reg_description) { |
| 80 DCHECK(reg_description); |
| 81 RegKey reg_ext(HKEY_CLASSES_ROOT, file_ext.c_str(), KEY_READ); |
| 82 std::wstring reg_app; |
| 83 if (reg_ext.ReadValue(NULL, ®_app) && !reg_app.empty()) { |
| 84 RegKey reg_link(HKEY_CLASSES_ROOT, reg_app.c_str(), KEY_READ); |
| 85 if (reg_link.ReadValue(NULL, reg_description)) |
| 86 return true; |
| 87 } |
| 88 return false; |
| 89 } |
| 90 |
| 91 // Set up a filter for a Save/Open dialog, which will consist of |file_ext| file |
| 92 // extensions (internally separated by semicolons), |ext_desc| as the text |
| 93 // descriptions of the |file_ext| types (optional), and (optionally) the default |
| 94 // 'All Files' view. The purpose of the filter is to show only files of a |
| 95 // particular type in a Windows Save/Open dialog box. The resulting filter is |
| 96 // returned. The filters created here are: |
| 97 // 1. only files that have 'file_ext' as their extension |
| 98 // 2. all files (only added if 'include_all_files' is true) |
| 99 // Example: |
| 100 // file_ext: { "*.txt", "*.htm;*.html" } |
| 101 // ext_desc: { "Text Document" } |
| 102 // returned: "Text Document\0*.txt\0HTML Document\0*.htm;*.html\0" |
| 103 // "All Files\0*.*\0\0" (in one big string) |
| 104 // If a description is not provided for a file extension, it will be retrieved |
| 105 // from the registry. If the file extension does not exist in the registry, it |
| 106 // will be omitted from the filter, as it is likely a bogus extension. |
| 107 std::wstring FormatFilterForExtensions( |
| 108 const std::vector<std::wstring>& file_ext, |
| 109 const std::vector<std::wstring>& ext_desc, |
| 110 bool include_all_files) { |
| 111 const std::wstring all_ext = L"*.*"; |
| 112 const std::wstring all_desc = l10n_util::GetString(IDS_APP_SAVEAS_ALL_FILES); |
| 113 |
| 114 DCHECK(file_ext.size() >= ext_desc.size()); |
| 115 |
| 116 std::wstring result; |
| 117 |
| 118 for (size_t i = 0; i < file_ext.size(); ++i) { |
| 119 std::wstring ext = file_ext[i]; |
| 120 std::wstring desc; |
| 121 if (i < ext_desc.size()) |
| 122 desc = ext_desc[i]; |
| 123 |
| 124 if (ext.empty()) { |
| 125 // Force something reasonable to appear in the dialog box if there is no |
| 126 // extension provided. |
| 127 include_all_files = true; |
| 128 continue; |
| 129 } |
| 130 |
| 131 if (desc.empty()) { |
| 132 DCHECK(ext.find(L'.') != std::wstring::npos); |
| 133 std::wstring first_extension = ext.substr(ext.find(L'.')); |
| 134 size_t first_separator_index = first_extension.find(L';'); |
| 135 if (first_separator_index != std::wstring::npos) |
| 136 first_extension = first_extension.substr(0, first_separator_index); |
| 137 |
| 138 // Find the extension name without the preceeding '.' character. |
| 139 std::wstring ext_name = first_extension; |
| 140 size_t ext_index = ext_name.find_first_not_of(L'.'); |
| 141 if (ext_index != std::wstring::npos) |
| 142 ext_name = ext_name.substr(ext_index); |
| 143 |
| 144 if (!GetRegistryDescriptionFromExtension(first_extension, &desc)) { |
| 145 // The extension doesn't exist in the registry. Create a description |
| 146 // based on the unknown extension type (i.e. if the extension is .qqq, |
| 147 // the we create a description "QQQ File (.qqq)"). |
| 148 include_all_files = true; |
| 149 desc = l10n_util::GetStringF(IDS_APP_SAVEAS_EXTENSION_FORMAT, |
| 150 l10n_util::ToUpper(ext_name), |
| 151 ext_name); |
| 152 } |
| 153 if (desc.empty()) |
| 154 desc = L"*." + ext_name; |
| 155 } |
| 156 |
| 157 result.append(desc.c_str(), desc.size() + 1); // Append NULL too. |
| 158 result.append(ext.c_str(), ext.size() + 1); |
| 159 } |
| 160 |
| 161 if (include_all_files) { |
| 162 result.append(all_desc.c_str(), all_desc.size() + 1); |
| 163 result.append(all_ext.c_str(), all_ext.size() + 1); |
| 164 } |
| 165 |
| 166 result.append(1, '\0'); // Double NULL required. |
| 167 return result; |
| 168 } |
| 169 |
| 170 // Enforce visible dialog box. |
| 171 UINT_PTR CALLBACK SaveAsDialogHook(HWND dialog, UINT message, |
| 172 WPARAM wparam, LPARAM lparam) { |
| 173 static const UINT kPrivateMessage = 0x2F3F; |
| 174 switch (message) { |
| 175 case WM_INITDIALOG: { |
| 176 // Do nothing here. Just post a message to defer actual processing. |
| 177 PostMessage(dialog, kPrivateMessage, 0, 0); |
| 178 return TRUE; |
| 179 } |
| 180 case kPrivateMessage: { |
| 181 // The dialog box is the parent of the current handle. |
| 182 HWND real_dialog = GetParent(dialog); |
| 183 |
| 184 // Retrieve the final size. |
| 185 RECT dialog_rect; |
| 186 GetWindowRect(real_dialog, &dialog_rect); |
| 187 |
| 188 // Verify that the upper left corner is visible. |
| 189 POINT point = { dialog_rect.left, dialog_rect.top }; |
| 190 HMONITOR monitor1 = MonitorFromPoint(point, MONITOR_DEFAULTTONULL); |
| 191 point.x = dialog_rect.right; |
| 192 point.y = dialog_rect.bottom; |
| 193 |
| 194 // Verify that the lower right corner is visible. |
| 195 HMONITOR monitor2 = MonitorFromPoint(point, MONITOR_DEFAULTTONULL); |
| 196 if (monitor1 && monitor2) |
| 197 return 0; |
| 198 |
| 199 // Some part of the dialog box is not visible, fix it by moving is to the |
| 200 // client rect position of the browser window. |
| 201 HWND parent_window = GetParent(real_dialog); |
| 202 if (!parent_window) |
| 203 return 0; |
| 204 WINDOWINFO parent_info; |
| 205 parent_info.cbSize = sizeof(WINDOWINFO); |
| 206 GetWindowInfo(parent_window, &parent_info); |
| 207 SetWindowPos(real_dialog, NULL, |
| 208 parent_info.rcClient.left, |
| 209 parent_info.rcClient.top, |
| 210 0, 0, // Size. |
| 211 SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOSIZE | |
| 212 SWP_NOZORDER); |
| 213 |
| 214 return 0; |
| 215 } |
| 216 } |
| 217 return 0; |
| 218 } |
| 219 |
| 220 // Prompt the user for location to save a file. |
| 221 // Callers should provide the filter string, and also a filter index. |
| 222 // The parameter |index| indicates the initial index of filter description |
| 223 // and filter pattern for the dialog box. If |index| is zero or greater than |
| 224 // the number of total filter types, the system uses the first filter in the |
| 225 // |filter| buffer. |index| is used to specify the initial selected extension, |
| 226 // and when done contains the extension the user chose. The parameter |
| 227 // |final_name| returns the file name which contains the drive designator, |
| 228 // path, file name, and extension of the user selected file name. |def_ext| is |
| 229 // the default extension to give to the file if the user did not enter an |
| 230 // extension. If |ignore_suggested_ext| is true, any file extension contained in |
| 231 // |suggested_name| will not be used to generate the file name. This is useful |
| 232 // in the case of saving web pages, where we know the extension type already and |
| 233 // where |suggested_name| may contain a '.' character as a valid part of the |
| 234 // name, thus confusing our extension detection code. |
| 235 bool SaveFileAsWithFilter(HWND owner, |
| 236 const std::wstring& suggested_name, |
| 237 const std::wstring& filter, |
| 238 const std::wstring& def_ext, |
| 239 bool ignore_suggested_ext, |
| 240 unsigned* index, |
| 241 std::wstring* final_name) { |
| 242 DCHECK(final_name); |
| 243 // Having an empty filter makes for a bad user experience. We should always |
| 244 // specify a filter when saving. |
| 245 DCHECK(!filter.empty()); |
| 246 std::wstring file_part = file_util::GetFilenameFromPath(suggested_name); |
| 247 |
| 248 // The size of the in/out buffer in number of characters we pass to win32 |
| 249 // GetSaveFileName. From MSDN "The buffer must be large enough to store the |
| 250 // path and file name string or strings, including the terminating NULL |
| 251 // character. ... The buffer should be at least 256 characters long.". |
| 252 // _IsValidPathComDlg does a copy expecting at most MAX_PATH, otherwise will |
| 253 // result in an error of FNERR_INVALIDFILENAME. So we should only pass the |
| 254 // API a buffer of at most MAX_PATH. |
| 255 wchar_t file_name[MAX_PATH]; |
| 256 base::wcslcpy(file_name, file_part.c_str(), arraysize(file_name)); |
| 257 |
| 258 OPENFILENAME save_as; |
| 259 // We must do this otherwise the ofn's FlagsEx may be initialized to random |
| 260 // junk in release builds which can cause the Places Bar not to show up! |
| 261 ZeroMemory(&save_as, sizeof(save_as)); |
| 262 save_as.lStructSize = sizeof(OPENFILENAME); |
| 263 save_as.hwndOwner = owner; |
| 264 save_as.hInstance = NULL; |
| 265 |
| 266 save_as.lpstrFilter = filter.empty() ? NULL : filter.c_str(); |
| 267 |
| 268 save_as.lpstrCustomFilter = NULL; |
| 269 save_as.nMaxCustFilter = 0; |
| 270 save_as.nFilterIndex = *index; |
| 271 save_as.lpstrFile = file_name; |
| 272 save_as.nMaxFile = arraysize(file_name); |
| 273 save_as.lpstrFileTitle = NULL; |
| 274 save_as.nMaxFileTitle = 0; |
| 275 |
| 276 // Set up the initial directory for the dialog. |
| 277 std::wstring directory = file_util::GetDirectoryFromPath(suggested_name); |
| 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 (win_util::GetWinVersion() < win_util::WINVERSION_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 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. |
| 352 bool SaveFileAs(HWND owner, |
| 353 const std::wstring& suggested_name, |
| 354 std::wstring* final_name) { |
| 355 std::wstring file_ext = file_util::GetFileExtensionFromPath(suggested_name); |
| 356 file_ext.insert(0, L"*."); |
| 357 std::wstring filter = FormatFilterForExtensions( |
| 358 std::vector<std::wstring>(1, file_ext), |
| 359 std::vector<std::wstring>(), |
| 360 true); |
| 361 unsigned index = 1; |
| 362 return SaveFileAsWithFilter(owner, |
| 363 suggested_name, |
| 364 filter, |
| 365 L"", |
| 366 false, |
| 367 &index, |
| 368 final_name); |
| 369 } |
| 370 |
| 371 } // namespace |
24 | 372 |
25 // Helpers to show certain types of Windows shell dialogs in a way that doesn't | 373 // Helpers to show certain types of Windows shell dialogs in a way that doesn't |
26 // block the UI of the entire app. | 374 // block the UI of the entire app. |
27 | 375 |
28 class ShellDialogThread : public base::Thread { | 376 class ShellDialogThread : public base::Thread { |
29 public: | 377 public: |
30 ShellDialogThread() : base::Thread("Chrome_ShellDialogThread") { } | 378 ShellDialogThread() : base::Thread("Chrome_ShellDialogThread") { } |
31 | 379 |
32 protected: | 380 protected: |
33 void Init() { | 381 void Init() { |
(...skipping 300 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
334 params.file_types.extensions[i]; | 682 params.file_types.extensions[i]; |
335 std::wstring ext_string; | 683 std::wstring ext_string; |
336 for (size_t j = 0; j < inner_exts.size(); ++j) { | 684 for (size_t j = 0; j < inner_exts.size(); ++j) { |
337 if (!ext_string.empty()) | 685 if (!ext_string.empty()) |
338 ext_string.push_back(L';'); | 686 ext_string.push_back(L';'); |
339 ext_string.append(L"*."); | 687 ext_string.append(L"*."); |
340 ext_string.append(inner_exts[j]); | 688 ext_string.append(inner_exts[j]); |
341 } | 689 } |
342 exts.push_back(ext_string); | 690 exts.push_back(ext_string); |
343 } | 691 } |
344 std::wstring filter = win_util::FormatFilterForExtensions( | 692 std::wstring filter = FormatFilterForExtensions( |
345 exts, | 693 exts, |
346 params.file_types.extension_description_overrides, | 694 params.file_types.extension_description_overrides, |
347 params.file_types.include_all_files); | 695 params.file_types.include_all_files); |
348 | 696 |
349 FilePath path = params.default_path; | 697 FilePath path = params.default_path; |
350 bool success = false; | 698 bool success = false; |
351 unsigned filter_index = params.file_type_index; | 699 unsigned filter_index = params.file_type_index; |
352 if (params.type == SELECT_FOLDER) { | 700 if (params.type == SELECT_FOLDER) { |
353 success = RunSelectFolderDialog(params.title, | 701 success = RunSelectFolderDialog(params.title, |
354 params.run_state.owner, | 702 params.run_state.owner, |
355 &path); | 703 &path); |
356 } else if (params.type == SELECT_SAVEAS_FILE) { | 704 } else if (params.type == SELECT_SAVEAS_FILE) { |
357 std::wstring path_as_wstring = path.ToWStringHack(); | 705 std::wstring path_as_wstring = path.ToWStringHack(); |
358 success = win_util::SaveFileAsWithFilter(params.run_state.owner, | 706 success = SaveFileAsWithFilter(params.run_state.owner, |
359 params.default_path.ToWStringHack(), filter, | 707 params.default_path.ToWStringHack(), filter, |
360 params.default_extension, false, &filter_index, &path_as_wstring); | 708 params.default_extension, false, &filter_index, &path_as_wstring); |
361 if (success) { | 709 if (success) { |
362 path = FilePath::FromWStringHack(path_as_wstring); | 710 path = FilePath::FromWStringHack(path_as_wstring); |
363 } | 711 } |
364 DisableOwner(params.run_state.owner); | 712 DisableOwner(params.run_state.owner); |
365 } else if (params.type == SELECT_OPEN_FILE) { | 713 } else if (params.type == SELECT_OPEN_FILE) { |
366 success = RunOpenFileDialog(params.title, filter, | 714 success = RunOpenFileDialog(params.title, filter, |
367 params.run_state.owner, &path); | 715 params.run_state.owner, &path); |
368 } else if (params.type == SELECT_OPEN_MULTI_FILE) { | 716 } else if (params.type == SELECT_OPEN_MULTI_FILE) { |
(...skipping 384 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
753 void SelectFontDialogImpl::FontNotSelected(void* params, RunState run_state) { | 1101 void SelectFontDialogImpl::FontNotSelected(void* params, RunState run_state) { |
754 if (listener_) | 1102 if (listener_) |
755 listener_->FontSelectionCanceled(params); | 1103 listener_->FontSelectionCanceled(params); |
756 EndRun(run_state); | 1104 EndRun(run_state); |
757 } | 1105 } |
758 | 1106 |
759 // static | 1107 // static |
760 SelectFontDialog* SelectFontDialog::Create(Listener* listener) { | 1108 SelectFontDialog* SelectFontDialog::Create(Listener* listener) { |
761 return new SelectFontDialogImpl(listener); | 1109 return new SelectFontDialogImpl(listener); |
762 } | 1110 } |
OLD | NEW |