Merge mozilla-central to autoland on a CLOSED TREE
[gecko.git] / widget / windows / nsFilePicker.cpp
blobd45922d3bea96f5bd1251a03d4791264624cfc8b
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "nsFilePicker.h"
9 #include <shlobj.h>
10 #include <shlwapi.h>
11 #include <cderr.h>
13 #include "mozilla/Assertions.h"
14 #include "mozilla/BackgroundHangMonitor.h"
15 #include "mozilla/ProfilerLabels.h"
16 #include "mozilla/UniquePtr.h"
17 #include "mozilla/WindowsVersion.h"
18 #include "nsReadableUtils.h"
19 #include "nsNetUtil.h"
20 #include "nsWindow.h"
21 #include "nsEnumeratorUtils.h"
22 #include "nsCRT.h"
23 #include "nsString.h"
24 #include "nsToolkit.h"
25 #include "WinUtils.h"
26 #include "nsPIDOMWindow.h"
28 #include "mozilla/widget/filedialog/WinFileDialogCommands.h"
30 using mozilla::UniquePtr;
32 using namespace mozilla::widget;
34 UniquePtr<char16_t[], nsFilePicker::FreeDeleter>
35 nsFilePicker::sLastUsedUnicodeDirectory;
37 #define MAX_EXTENSION_LENGTH 10
39 ///////////////////////////////////////////////////////////////////////////////
40 // Helper classes
42 // Manages matching PickerOpen/PickerClosed calls on the parent widget.
43 class AutoWidgetPickerState {
44 public:
45 explicit AutoWidgetPickerState(nsIWidget* aWidget)
46 : mWindow(static_cast<nsWindow*>(aWidget)) {
47 PickerState(true);
50 ~AutoWidgetPickerState() { PickerState(false); }
52 private:
53 void PickerState(bool aFlag) {
54 if (mWindow) {
55 if (aFlag)
56 mWindow->PickerOpen();
57 else
58 mWindow->PickerClosed();
61 RefPtr<nsWindow> mWindow;
64 ///////////////////////////////////////////////////////////////////////////////
65 // nsIFilePicker
67 nsFilePicker::nsFilePicker() : mSelectedType(1) {}
69 NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
71 NS_IMETHODIMP nsFilePicker::Init(mozIDOMWindowProxy* aParent,
72 const nsAString& aTitle,
73 nsIFilePicker::Mode aMode) {
74 nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(aParent);
75 nsIDocShell* docShell = window ? window->GetDocShell() : nullptr;
76 mLoadContext = do_QueryInterface(docShell);
78 return nsBaseFilePicker::Init(aParent, aTitle, aMode);
82 * Folder picker invocation
86 * Show a folder picker.
88 * @param aInitialDir The initial directory, the last used directory will be
89 * used if left blank.
90 * @return true if a file was selected successfully.
92 bool nsFilePicker::ShowFolderPicker(const nsString& aInitialDir) {
93 RefPtr<IFileOpenDialog> dialog;
94 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr,
95 CLSCTX_INPROC_SERVER, IID_IFileOpenDialog,
96 getter_AddRefs(dialog)))) {
97 return false;
100 namespace fd = ::mozilla::widget::filedialog;
101 nsTArray<fd::Command> commands = {
102 fd::SetOptions(FOS_PICKFOLDERS),
103 fd::SetTitle(mTitle),
106 if (!mOkButtonLabel.IsEmpty()) {
107 commands.AppendElement(fd::SetOkButtonLabel(mOkButtonLabel));
110 if (!aInitialDir.IsEmpty()) {
111 commands.AppendElement(fd::SetFolder(aInitialDir));
115 if (NS_FAILED(fd::ApplyCommands(dialog, commands))) {
116 return false;
119 ScopedRtlShimWindow shim(mParentWidget.get());
120 mozilla::BackgroundHangMonitor().NotifyWait();
122 if (FAILED(dialog->Show(shim.get()))) {
123 return false;
127 auto result = fd::GetFolderResults(dialog.get());
128 if (result.isErr()) {
129 return false;
132 mUnicodeFile = result.unwrap();
133 return true;
137 * File open and save picker invocation
141 * Show a file picker.
143 * @param aInitialDir The initial directory, the last used directory will be
144 * used if left blank.
145 * @return true if a file was selected successfully.
147 bool nsFilePicker::ShowFilePicker(const nsString& aInitialDir) {
148 AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER);
150 RefPtr<IFileDialog> dialog;
151 if (mMode != modeSave) {
152 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr,
153 CLSCTX_INPROC_SERVER, IID_IFileOpenDialog,
154 getter_AddRefs(dialog)))) {
155 return false;
157 } else {
158 if (FAILED(CoCreateInstance(CLSID_FileSaveDialog, nullptr,
159 CLSCTX_INPROC_SERVER, IID_IFileSaveDialog,
160 getter_AddRefs(dialog)))) {
161 return false;
165 namespace fd = ::mozilla::widget::filedialog;
166 nsTArray<fd::Command> commands;
167 // options
169 FILEOPENDIALOGOPTIONS fos = 0;
170 fos |= FOS_SHAREAWARE | FOS_OVERWRITEPROMPT | FOS_FORCEFILESYSTEM;
172 // Handle add to recent docs settings
173 if (IsPrivacyModeEnabled() || !mAddToRecentDocs) {
174 fos |= FOS_DONTADDTORECENT;
177 // mode specific
178 switch (mMode) {
179 case modeOpen:
180 fos |= FOS_FILEMUSTEXIST;
181 break;
183 case modeOpenMultiple:
184 fos |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT;
185 break;
187 case modeSave:
188 fos |= FOS_NOREADONLYRETURN;
189 // Don't follow shortcuts when saving a shortcut, this can be used
190 // to trick users (bug 271732)
191 if (IsDefaultPathLink()) fos |= FOS_NODEREFERENCELINKS;
192 break;
194 case modeGetFolder:
195 MOZ_ASSERT(false, "file-picker opened in directory-picker mode");
196 return false;
199 commands.AppendElement(fd::SetOptions(fos));
202 // initial strings
204 // title
205 commands.AppendElement(fd::SetTitle(mTitle));
207 // default filename
208 if (!mDefaultFilename.IsEmpty()) {
209 // Prevent the shell from expanding environment variables by removing
210 // the % characters that are used to delimit them.
211 nsAutoString sanitizedFilename(mDefaultFilename);
212 sanitizedFilename.ReplaceChar('%', '_');
214 commands.AppendElement(fd::SetFileName(sanitizedFilename));
217 // default extension to append to new files
218 if (!mDefaultExtension.IsEmpty()) {
219 // We don't want environment variables expanded in the extension either.
220 nsAutoString sanitizedExtension(mDefaultExtension);
221 sanitizedExtension.ReplaceChar('%', '_');
223 commands.AppendElement(fd::SetDefaultExtension(sanitizedExtension));
224 } else if (IsDefaultPathHtml()) {
225 commands.AppendElement(fd::SetDefaultExtension(u"html"_ns));
228 // initial location
229 if (!aInitialDir.IsEmpty()) {
230 commands.AppendElement(fd::SetFolder(aInitialDir));
233 // filter types and the default index
234 if (!mFilterList.IsEmpty()) {
235 nsTArray<fd::ComDlgFilterSpec> fileTypes;
236 for (auto const& filter : mFilterList) {
237 fileTypes.EmplaceBack(filter.title, filter.filter);
239 commands.AppendElement(fd::SetFileTypes(std::move(fileTypes)));
240 commands.AppendElement(fd::SetFileTypeIndex(mSelectedType));
243 // display
245 if (NS_FAILED(fd::ApplyCommands(dialog, commands))) {
246 return false;
249 ScopedRtlShimWindow shim(mParentWidget.get());
250 AutoWidgetPickerState awps(mParentWidget);
252 mozilla::BackgroundHangMonitor().NotifyWait();
253 if (FAILED(dialog->Show(shim.get()))) {
254 return false;
258 // results
259 auto result_ = fd::GetFileResults(dialog.get());
260 if (result_.isErr()) {
261 return false;
263 auto result = result_.unwrap();
265 // Remember what filter type the user selected
266 mSelectedType = result.selectedFileTypeIndex();
268 auto const& paths = result.paths();
270 // single selection
271 if (mMode != modeOpenMultiple) {
272 if (!paths.IsEmpty()) {
273 MOZ_ASSERT(paths.Length() == 1);
274 mUnicodeFile = paths[0];
275 return true;
277 return false;
280 // multiple selection
281 for (auto const& str : paths) {
282 nsCOMPtr<nsIFile> file;
283 if (NS_SUCCEEDED(NS_NewLocalFile(str, false, getter_AddRefs(file)))) {
284 mFiles.AppendObject(file);
287 return true;
290 ///////////////////////////////////////////////////////////////////////////////
291 // nsIFilePicker impl.
293 nsresult nsFilePicker::ShowW(nsIFilePicker::ResultCode* aReturnVal) {
294 NS_ENSURE_ARG_POINTER(aReturnVal);
296 *aReturnVal = returnCancel;
298 nsAutoString initialDir;
299 if (mDisplayDirectory) mDisplayDirectory->GetPath(initialDir);
301 // If no display directory, re-use the last one.
302 if (initialDir.IsEmpty()) {
303 // Allocate copy of last used dir.
304 initialDir = sLastUsedUnicodeDirectory.get();
307 // Clear previous file selections
308 mUnicodeFile.Truncate();
309 mFiles.Clear();
311 // On Win10, the picker doesn't support per-monitor DPI, so we open it
312 // with our context set temporarily to system-dpi-aware
313 WinUtils::AutoSystemDpiAware dpiAwareness;
315 bool result = false;
316 if (mMode == modeGetFolder) {
317 result = ShowFolderPicker(initialDir);
318 } else {
319 result = ShowFilePicker(initialDir);
322 // exit, and return returnCancel in aReturnVal
323 if (!result) return NS_OK;
325 RememberLastUsedDirectory();
327 nsIFilePicker::ResultCode retValue = returnOK;
328 if (mMode == modeSave) {
329 // Windows does not return resultReplace, we must check if file
330 // already exists.
331 nsCOMPtr<nsIFile> file;
332 nsresult rv = NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file));
334 bool flag = false;
335 if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(file->Exists(&flag)) && flag) {
336 retValue = returnReplace;
340 *aReturnVal = retValue;
341 return NS_OK;
344 nsresult nsFilePicker::Show(nsIFilePicker::ResultCode* aReturnVal) {
345 return ShowW(aReturnVal);
348 NS_IMETHODIMP
349 nsFilePicker::GetFile(nsIFile** aFile) {
350 NS_ENSURE_ARG_POINTER(aFile);
351 *aFile = nullptr;
353 if (mUnicodeFile.IsEmpty()) return NS_OK;
355 nsCOMPtr<nsIFile> file;
356 nsresult rv = NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file));
357 if (NS_FAILED(rv)) {
358 return rv;
361 file.forget(aFile);
362 return NS_OK;
365 NS_IMETHODIMP
366 nsFilePicker::GetFileURL(nsIURI** aFileURL) {
367 *aFileURL = nullptr;
368 nsCOMPtr<nsIFile> file;
369 nsresult rv = GetFile(getter_AddRefs(file));
370 if (!file) return rv;
372 return NS_NewFileURI(aFileURL, file);
375 NS_IMETHODIMP
376 nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
377 NS_ENSURE_ARG_POINTER(aFiles);
378 return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
381 // Get the file + path
382 NS_IMETHODIMP
383 nsBaseWinFilePicker::SetDefaultString(const nsAString& aString) {
384 mDefaultFilePath = aString;
386 // First, make sure the file name is not too long.
387 int32_t nameLength;
388 int32_t nameIndex = mDefaultFilePath.RFind(u"\\");
389 if (nameIndex == kNotFound)
390 nameIndex = 0;
391 else
392 nameIndex++;
393 nameLength = mDefaultFilePath.Length() - nameIndex;
394 mDefaultFilename.Assign(Substring(mDefaultFilePath, nameIndex));
396 if (nameLength > MAX_PATH) {
397 int32_t extIndex = mDefaultFilePath.RFind(u".");
398 if (extIndex == kNotFound) extIndex = mDefaultFilePath.Length();
400 // Let's try to shave the needed characters from the name part.
401 int32_t charsToRemove = nameLength - MAX_PATH;
402 if (extIndex - nameIndex >= charsToRemove) {
403 mDefaultFilePath.Cut(extIndex - charsToRemove, charsToRemove);
407 // Then, we need to replace illegal characters. At this stage, we cannot
408 // replace the backslash as the string might represent a file path.
409 mDefaultFilePath.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-');
410 mDefaultFilename.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-');
412 return NS_OK;
415 NS_IMETHODIMP
416 nsBaseWinFilePicker::GetDefaultString(nsAString& aString) {
417 return NS_ERROR_FAILURE;
420 // The default extension to use for files
421 NS_IMETHODIMP
422 nsBaseWinFilePicker::GetDefaultExtension(nsAString& aExtension) {
423 aExtension = mDefaultExtension;
424 return NS_OK;
427 NS_IMETHODIMP
428 nsBaseWinFilePicker::SetDefaultExtension(const nsAString& aExtension) {
429 mDefaultExtension = aExtension;
430 return NS_OK;
433 // Set the filter index
434 NS_IMETHODIMP
435 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
436 // Windows' filter index is 1-based, we use a 0-based system.
437 *aFilterIndex = mSelectedType - 1;
438 return NS_OK;
441 NS_IMETHODIMP
442 nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
443 // Windows' filter index is 1-based, we use a 0-based system.
444 mSelectedType = aFilterIndex + 1;
445 return NS_OK;
448 void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
449 mParentWidget = aParent;
450 mTitle.Assign(aTitle);
453 NS_IMETHODIMP
454 nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
455 nsString sanitizedFilter(aFilter);
456 sanitizedFilter.ReplaceChar('%', '_');
458 if (sanitizedFilter == u"..apps"_ns) {
459 sanitizedFilter = u"*.exe;*.com"_ns;
460 } else {
461 sanitizedFilter.StripWhitespace();
462 if (sanitizedFilter == u"*"_ns) {
463 sanitizedFilter = u"*.*"_ns;
466 mFilterList.AppendElement(
467 Filter{.title = nsString(aTitle), .filter = std::move(sanitizedFilter)});
468 return NS_OK;
471 void nsFilePicker::RememberLastUsedDirectory() {
472 if (IsPrivacyModeEnabled()) {
473 // Don't remember the directory if private browsing was in effect
474 return;
477 nsCOMPtr<nsIFile> file;
478 if (NS_FAILED(NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file)))) {
479 NS_WARNING("RememberLastUsedDirectory failed to init file path.");
480 return;
483 nsCOMPtr<nsIFile> dir;
484 nsAutoString newDir;
485 if (NS_FAILED(file->GetParent(getter_AddRefs(dir))) ||
486 !(mDisplayDirectory = dir) ||
487 NS_FAILED(mDisplayDirectory->GetPath(newDir)) || newDir.IsEmpty()) {
488 NS_WARNING("RememberLastUsedDirectory failed to get parent directory.");
489 return;
492 sLastUsedUnicodeDirectory.reset(ToNewUnicode(newDir));
495 bool nsFilePicker::IsPrivacyModeEnabled() {
496 return mLoadContext && mLoadContext->UsePrivateBrowsing();
499 bool nsFilePicker::IsDefaultPathLink() {
500 NS_ConvertUTF16toUTF8 ext(mDefaultFilePath);
501 ext.Trim(" .", false, true); // watch out for trailing space and dots
502 ToLowerCase(ext);
503 return StringEndsWith(ext, ".lnk"_ns) || StringEndsWith(ext, ".pif"_ns) ||
504 StringEndsWith(ext, ".url"_ns);
507 bool nsFilePicker::IsDefaultPathHtml() {
508 int32_t extIndex = mDefaultFilePath.RFind(u".");
509 if (extIndex >= 0) {
510 nsAutoString ext;
511 mDefaultFilePath.Right(ext, mDefaultFilePath.Length() - extIndex);
512 if (ext.LowerCaseEqualsLiteral(".htm") ||
513 ext.LowerCaseEqualsLiteral(".html") ||
514 ext.LowerCaseEqualsLiteral(".shtml"))
515 return true;
517 return false;