Bug 1164310, part 4 - Implement the new HTMLInputElement API including the new Promise returning GetFilesAndDirectories. r=baku
authorJonathan Watt <jwatt@jwatt.org>
Fri, 10 Jul 2015 18:55:52 +0100
changeset 253939 c6a6d9d77dd552e503134368a98419fd3c0ab0e4
parent 253938 0a945cb91184980c4128809146d9ffadba7bc98e
child 253940 728bc2eed910e8ed0ee2513883a1531522fb07bc
push id29083
push userkwierso@gmail.com
push dateTue, 21 Jul 2015 22:49:28 +0000
treeherdermozilla-central@1875a5584e5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku
bugs1164310
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1164310, part 4 - Implement the new HTMLInputElement API including the new Promise returning GetFilesAndDirectories. r=baku
dom/filesystem/FileSystemBase.h
dom/html/HTMLInputElement.cpp
dom/html/HTMLInputElement.h
dom/ipc/FilePickerParent.cpp
dom/webidl/HTMLInputElement.webidl
widget/nsBaseFilePicker.cpp
--- a/dom/filesystem/FileSystemBase.h
+++ b/dom/filesystem/FileSystemBase.h
@@ -50,16 +50,22 @@ public:
 
   /*
    * Get the virtual name of the root directory. This name will be exposed to
    * the content page.
    */
   virtual void
   GetRootName(nsAString& aRetval) const = 0;
 
+  const nsAString&
+  GetLocalRootPath() const
+  {
+    return mLocalRootPath;
+  }
+
   bool
   IsShutdown() const
   {
     return mShutdown;
   }
 
   virtual bool
   IsSafeFile(nsIFile* aFile) const;
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -5,17 +5,21 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/HTMLInputElement.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/dom/Date.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/OSFileSystem.h"
 #include "nsAttrValueInlines.h"
+#include "nsCRTGlue.h"
 
 #include "nsIDOMHTMLInputElement.h"
 #include "nsITextControlElement.h"
 #include "nsIDOMNSEditableElement.h"
 #include "nsIRadioVisitor.h"
 #include "nsIPhonetic.h"
 
 #include "mozilla/Telemetry.h"
@@ -368,17 +372,18 @@ HTMLInputElement::nsFilePickerShownCallb
     return NS_OK;
   }
 
   int16_t mode;
   mFilePicker->GetMode(&mode);
 
   // Collect new selected filenames
   nsTArray<nsRefPtr<File>> newFiles;
-  if (mode == static_cast<int16_t>(nsIFilePicker::modeOpenMultiple)) {
+  if (mode == static_cast<int16_t>(nsIFilePicker::modeOpenMultiple) ||
+      mode == static_cast<int16_t>(nsIFilePicker::modeGetFolder)) {
     nsCOMPtr<nsISimpleEnumerator> iter;
     nsresult rv = mFilePicker->GetDomfiles(getter_AddRefs(iter));
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (!iter) {
       return NS_OK;
     }
 
@@ -2871,17 +2876,19 @@ HTMLInputElement::Focus(ErrorResult& aEr
     }
   }
 
   if (mType != NS_FORM_INPUT_FILE) {
     nsGenericHTMLElement::Focus(aError);
     return;
   }
 
-  // For file inputs, focus the button instead.
+  // For file inputs, focus the first button instead. In the case of there
+  // being two buttons (when the picker is a directory picker) the user can
+  // tab to the next one.
   nsIFrame* frame = GetPrimaryFrame();
   if (frame) {
     for (nsIFrame* childFrame = frame->GetFirstPrincipalChild();
          childFrame;
          childFrame = childFrame->GetNextSibling()) {
       // See if the child is a button control.
       nsCOMPtr<nsIFormControl> formCtrl =
         do_QueryInterface(childFrame->GetContent());
@@ -3539,17 +3546,31 @@ HTMLInputElement::MaybeInitPickers(Event
   if (aVisitor.mEvent->mFlags.mDefaultPrevented) {
     return NS_OK;
   }
   WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
   if (!(mouseEvent && mouseEvent->IsLeftClickEvent())) {
     return NS_OK;
   }
   if (mType == NS_FORM_INPUT_FILE) {
-    return InitFilePicker(FILE_PICKER_FILE);
+    // If the user clicked on the "Choose folder..." button we open the
+    // directory picker, else we open the file picker.
+    FilePickerType type = FILE_PICKER_FILE;
+    nsCOMPtr<nsIContent> target =
+      do_QueryInterface(aVisitor.mEvent->originalTarget);
+    if (target &&
+        target->GetParent() == this &&
+        target->IsRootOfNativeAnonymousSubtree() &&
+        target->HasAttr(kNameSpaceID_None, nsGkAtoms::directory)) {
+      MOZ_ASSERT(Preferences::GetBool("dom.input.dirpicker", false),
+                 "No API or UI should have been exposed to allow this code to "
+                 "be reached");
+      type = FILE_PICKER_DIRECTORY;
+    }
+    return InitFilePicker(type);
   }
   if (mType == NS_FORM_INPUT_COLOR) {
     return InitColorPicker();
   }
   return NS_OK;
 }
 
 nsresult
@@ -4792,16 +4813,120 @@ HTMLInputElement::IsAttributeMapped(cons
 
 nsMapRuleToAttributesFunc
 HTMLInputElement::GetAttributeMappingFunction() const
 {
   return &MapAttributesIntoRule;
 }
 
 
+// Directory picking methods:
+
+bool
+HTMLInputElement::IsFilesAndDirectoriesSupported() const
+{
+  // This method is supposed to return true if a file and directory picker
+  // supports the selection of both files and directories *at the same time*.
+  // Only Mac currently supports that. We could implement it for Mac, but
+  // currently we do not.
+  return false;
+}
+
+void
+HTMLInputElement::ChooseDirectory(ErrorResult& aRv)
+{
+  if (mType != NS_FORM_INPUT_FILE) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return;
+  }
+  InitFilePicker(
+#if defined(ANDROID) || defined(MOZ_B2G)
+                 // No native directory picker - redirect to plain file picker
+                 FILE_PICKER_FILE
+#else
+                 FILE_PICKER_DIRECTORY
+#endif
+                 );
+}
+
+static already_AddRefed<OSFileSystem>
+MakeOrReuseFileSystem(const nsAString& aNewLocalRootPath,
+                      OSFileSystem* aFS,
+                      nsPIDOMWindow* aWindow)
+{
+  MOZ_ASSERT(aWindow);
+
+  nsRefPtr<OSFileSystem> fs;
+  if (aFS) {
+    const nsAString& prevLocalRootPath = aFS->GetLocalRootPath();
+    if (aNewLocalRootPath == prevLocalRootPath) {
+      fs = aFS;
+    }
+  }
+  if (!fs) {
+    fs = new OSFileSystem(aNewLocalRootPath);
+    fs->Init(aWindow);
+  }
+  return fs.forget();
+}
+
+already_AddRefed<Promise>
+HTMLInputElement::GetFilesAndDirectories(ErrorResult& aRv)
+{
+  nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+  MOZ_ASSERT(global);
+  if (!global) {
+    return nullptr;
+  }
+
+  nsRefPtr<Promise> p = Promise::Create(global, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  const nsTArray<nsRefPtr<File>>& filesAndDirs = GetFilesInternal();
+
+  Sequence<OwningFileOrDirectory> filesAndDirsSeq;
+
+  if (!filesAndDirsSeq.SetLength(filesAndDirs.Length(), mozilla::fallible_t())) {
+    p->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+    return p.forget();
+  }
+
+  nsPIDOMWindow* window = OwnerDoc()->GetInnerWindow();
+  nsRefPtr<OSFileSystem> fs;
+  for (uint32_t i = 0; i < filesAndDirs.Length(); ++i) {
+    if (filesAndDirs[i]->IsDirectory()) {
+#if defined(ANDROID) || defined(MOZ_B2G)
+      MOZ_ASSERT(false,
+                 "Directory picking should have been redirected to normal "
+                 "file picking for platforms that don't have a directory "
+                 "picker");
+#endif
+      nsAutoString path;
+      filesAndDirs[i]->GetMozFullPathInternal(path, aRv);
+      if (aRv.Failed()) {
+        return nullptr;
+      }
+      int32_t leafSeparatorIndex = path.RFind(FILE_PATH_SEPARATOR);
+      nsDependentSubstring dirname = Substring(path, 0, leafSeparatorIndex);
+      nsDependentSubstring basename = Substring(path, leafSeparatorIndex);
+      fs = MakeOrReuseFileSystem(dirname, fs, window);
+      filesAndDirsSeq[i].SetAsDirectory() = new Directory(fs, basename);
+    } else {
+      filesAndDirsSeq[i].SetAsFile() = filesAndDirs[i];
+    }
+  }
+
+  p->MaybeResolve(filesAndDirsSeq);
+
+  return p.forget();
+}
+
+
 // Controllers Methods
 
 nsIControllers*
 HTMLInputElement::GetControllers(ErrorResult& aRv)
 {
   //XXX: what about type "file"?
   if (IsSingleLineTextControl(false))
   {
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -14,16 +14,17 @@
 #include "nsITextControlElement.h"
 #include "nsITimer.h"
 #include "nsIPhonetic.h"
 #include "nsIDOMNSEditableElement.h"
 #include "nsCOMPtr.h"
 #include "nsIConstraintValidation.h"
 #include "mozilla/dom/HTMLFormElement.h" // for HasEverTriedInvalidSubmit()
 #include "mozilla/dom/HTMLInputElementBinding.h"
+#include "mozilla/dom/Promise.h"
 #include "nsIFilePicker.h"
 #include "nsIContentPrefService2.h"
 #include "mozilla/Decimal.h"
 #include "nsContentUtils.h"
 #include "nsTextEditorState.h"
 
 class nsIRadioGroupContainer;
 class nsIRadioVisitor;
@@ -680,16 +681,32 @@ public:
 
   void SetRangeText(const nsAString& aReplacement, ErrorResult& aRv);
 
   void SetRangeText(const nsAString& aReplacement, uint32_t aStart,
                     uint32_t aEnd, const SelectionMode& aSelectMode,
                     ErrorResult& aRv, int32_t aSelectionStart = -1,
                     int32_t aSelectionEnd = -1);
 
+  bool DirectoryAttr() const
+  {
+    return HasAttr(kNameSpaceID_None, nsGkAtoms::directory);
+  }
+
+  void SetDirectoryAttr(bool aValue, ErrorResult& aRv)
+  {
+    SetHTMLBoolAttr(nsGkAtoms::directory, aValue, aRv);
+  }
+
+  bool IsFilesAndDirectoriesSupported() const;
+
+  already_AddRefed<Promise> GetFilesAndDirectories(ErrorResult& aRv);
+
+  void ChooseDirectory(ErrorResult& aRv);
+
   // XPCOM GetAlign() is OK
   void SetAlign(const nsAString& aValue, ErrorResult& aRv)
   {
     SetHTMLAttr(nsGkAtoms::align, aValue, aRv);
   }
 
   // XPCOM GetUseMap() is OK
   void SetUseMap(const nsAString& aValue, ErrorResult& aRv)
--- a/dom/ipc/FilePickerParent.cpp
+++ b/dom/ipc/FilePickerParent.cpp
@@ -86,16 +86,17 @@ FilePickerParent::FileSizeAndDateRunnabl
     return NS_OK;
   }
 
   // We're not on the main thread, so do the stat().
   for (unsigned i = 0; i < mBlobs.Length(); i++) {
     ErrorResult rv;
     mBlobs[i]->GetSize(rv);
     mBlobs[i]->GetLastModified(rv);
+    mBlobs[i]->LookupAndCacheIsDirectory();
   }
 
   // Dispatch ourselves back on the main thread.
   if (NS_FAILED(NS_DispatchToMainThread(this))) {
     // It's hard to see how we can recover gracefully in this case. The child
     // process is waiting for an IPC, but that can only happen on the main
     // thread.
     MOZ_CRASH();
@@ -133,17 +134,18 @@ FilePickerParent::Done(int16_t aResult)
   mResult = aResult;
 
   if (mResult != nsIFilePicker::returnOK) {
     unused << Send__delete__(this, void_t(), mResult);
     return;
   }
 
   nsTArray<nsRefPtr<BlobImpl>> blobs;
-  if (mMode == nsIFilePicker::modeOpenMultiple) {
+  if (mMode == nsIFilePicker::modeOpenMultiple ||
+      mMode == nsIFilePicker::modeGetFolder) {
     nsCOMPtr<nsISimpleEnumerator> iter;
     NS_ENSURE_SUCCESS_VOID(mFilePicker->GetFiles(getter_AddRefs(iter)));
 
     nsCOMPtr<nsISupports> supports;
     bool loop = true;
     while (NS_SUCCEEDED(iter->HasMoreElements(&loop)) && loop) {
       iter->GetNext(getter_AddRefs(supports));
       if (supports) {
@@ -159,16 +161,17 @@ FilePickerParent::Done(int16_t aResult)
     if (file) {
       nsRefPtr<BlobImpl> blobimpl = new BlobImplFile(file);
       blobs.AppendElement(blobimpl);
     }
   }
 
   MOZ_ASSERT(!mRunnable);
   mRunnable = new FileSizeAndDateRunnable(this, blobs);
+  // Dispatch to background thread to do I/O:
   if (!mRunnable->Dispatch()) {
     unused << Send__delete__(this, void_t(), nsIFilePicker::returnCancel);
   }
 }
 
 bool
 FilePickerParent::CreateFilePicker()
 {
--- a/dom/webidl/HTMLInputElement.webidl
+++ b/dom/webidl/HTMLInputElement.webidl
@@ -184,16 +184,30 @@ partial interface HTMLInputElement {
 
   // This is similar to set .value on nsIDOMInput/TextAreaElements, but handling
   // of the value change is closer to the normal user input, so 'change' event
   // for example will be dispatched when focusing out the element.
   [ChromeOnly]
   void setUserInput(DOMString input);
 };
 
+partial interface HTMLInputElement {
+  [Pref="dom.input.dirpicker", BinaryName="DirectoryAttr", SetterThrows]
+  attribute boolean directory;
+
+  [Pref="dom.input.dirpicker"]
+  readonly attribute boolean isFilesAndDirectoriesSupported;
+
+  [Throws, Pref="dom.input.dirpicker"]
+  Promise<sequence<(File or Directory)>> getFilesAndDirectories();
+
+  [Throws, Pref="dom.input.dirpicker"]
+  void chooseDirectory();
+};
+
 [NoInterfaceObject]
 interface MozPhonetic {
   [Pure, ChromeOnly]
   readonly attribute DOMString phonetic;
 };
 
 HTMLInputElement implements MozImageLoadingContent;
 HTMLInputElement implements MozPhonetic;
--- a/widget/nsBaseFilePicker.cpp
+++ b/widget/nsBaseFilePicker.cpp
@@ -70,19 +70,21 @@ private:
 };
 
 class nsBaseFilePickerEnumerator : public nsISimpleEnumerator
 {
 public:
   NS_DECL_ISUPPORTS
 
   explicit nsBaseFilePickerEnumerator(nsPIDOMWindow* aParent,
-                                      nsISimpleEnumerator* iterator)
+                                      nsISimpleEnumerator* iterator,
+                                      int16_t aMode)
     : mIterator(iterator)
     , mParent(aParent)
+    , mMode(aMode)
   {}
 
   NS_IMETHOD
   GetNext(nsISupports** aResult) override
   {
     nsCOMPtr<nsISupports> tmp;
     nsresult rv = mIterator->GetNext(getter_AddRefs(tmp));
     NS_ENSURE_SUCCESS(rv, rv);
@@ -91,34 +93,58 @@ public:
       return NS_OK;
     }
 
     nsCOMPtr<nsIFile> localFile = do_QueryInterface(tmp);
     if (!localFile) {
       return NS_ERROR_FAILURE;
     }
 
-    nsCOMPtr<nsIDOMBlob> domFile = File::CreateFromFile(mParent, localFile);
-    domFile.forget(aResult);
+    nsRefPtr<File> domFile = File::CreateFromFile(mParent, localFile);
+
+    // Right now we're on the main thread of the chrome process. We need
+    // to call SetIsDirectory on the BlobImpl, but it's preferable not to
+    // call nsIFile::IsDirectory to determine what argument to pass since
+    // IsDirectory does synchronous I/O. It's true that since we've just
+    // been called synchronously directly after nsIFilePicker::Show blocked
+    // the main thread while the picker was being shown and the OS did file
+    // system access, doing more I/O to stat the selected files probably
+    // wouldn't be the end of the world. However, we can simply check
+    // mMode and avoid calling IsDirectory.
+    //
+    // In future we may take advantage of OS X's ability to allow both
+    // files and directories to be picked at the same time, so we do assert
+    // in debug builds that the mMode trick produces the correct results.
+    // If we do add that support maybe it's better to use IsDirectory
+    // directly, but in an nsRunnable punted off to a background thread.
+#ifdef DEBUG
+    bool isDir;
+    localFile->IsDirectory(&isDir);
+    MOZ_ASSERT(isDir == (mMode == nsIFilePicker::modeGetFolder));
+#endif
+    domFile->Impl()->SetIsDirectory(mMode == nsIFilePicker::modeGetFolder);
+
+    nsCOMPtr<nsIDOMBlob>(domFile).forget(aResult);
     return NS_OK;
   }
 
   NS_IMETHOD
   HasMoreElements(bool* aResult) override
   {
     return mIterator->HasMoreElements(aResult);
   }
 
 protected:
   virtual ~nsBaseFilePickerEnumerator()
   {}
 
 private:
   nsCOMPtr<nsISimpleEnumerator> mIterator;
   nsCOMPtr<nsPIDOMWindow> mParent;
+  int16_t mMode;
 };
 
 NS_IMPL_ISUPPORTS(nsBaseFilePickerEnumerator, nsISimpleEnumerator)
 
 nsBaseFilePicker::nsBaseFilePicker()
   : mAddToRecentDocs(true)
   , mMode(nsIFilePicker::modeOpen)
 {
@@ -330,14 +356,14 @@ nsBaseFilePicker::GetDomfile(nsISupports
 NS_IMETHODIMP
 nsBaseFilePicker::GetDomfiles(nsISimpleEnumerator** aDomfiles)
 {
   nsCOMPtr<nsISimpleEnumerator> iter;
   nsresult rv = GetFiles(getter_AddRefs(iter));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsRefPtr<nsBaseFilePickerEnumerator> retIter =
-    new nsBaseFilePickerEnumerator(mParent, iter);
+    new nsBaseFilePickerEnumerator(mParent, iter, mMode);
 
   retIter.forget(aDomfiles);
   return NS_OK;
 }