Bug 907767 - Make HTMLInputElement::OpenDirectoryPicker dispatch progress events. r=smaug
authorJonathan Watt <jwatt@jwatt.org>
Wed, 04 Sep 2013 15:21:32 +0100
changeset 146406 ac51f4fe929947da50270ee743ab1387860301a2
parent 146405 0452b5b504d09cdb1882bd22effbdb960c84dde0
child 146407 4ab57d0318fff7d71ac795724939b6faeccf16a8
push id25260
push userryanvm@gmail.com
push dateWed, 11 Sep 2013 00:29:30 +0000
treeherdermozilla-central@f73bed2856a8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs907767
milestone26.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 907767 - Make HTMLInputElement::OpenDirectoryPicker dispatch progress events. r=smaug
content/html/content/src/HTMLInputElement.cpp
content/html/content/src/HTMLInputElement.h
--- a/content/html/content/src/HTMLInputElement.cpp
+++ b/content/html/content/src/HTMLInputElement.cpp
@@ -19,16 +19,17 @@
 
 #include "nsIControllers.h"
 #include "nsIStringBundle.h"
 #include "nsFocusManager.h"
 #include "nsPIDOMWindow.h"
 #include "nsContentCID.h"
 #include "nsIComponentManager.h"
 #include "nsIDOMHTMLFormElement.h"
+#include "nsIDOMProgressEvent.h"
 #include "nsGkAtoms.h"
 #include "nsStyleConsts.h"
 #include "nsPresContext.h"
 #include "nsMappedAttributes.h"
 #include "nsIFormControl.h"
 #include "nsIForm.h"
 #include "nsFormSubmission.h"
 #include "nsFormSubmissionConstants.h"
@@ -209,16 +210,19 @@ const Decimal HTMLInputElement::kStepAny
 #define NS_INPUT_ELEMENT_STATE_IID                 \
 { /* dc3b3d14-23e2-4479-b513-7b369343e3a0 */       \
   0xdc3b3d14,                                      \
   0x23e2,                                          \
   0x4479,                                          \
   {0xb5, 0x13, 0x7b, 0x36, 0x93, 0x43, 0xe3, 0xa0} \
 }
 
+#define PROGRESS_STR "progress"
+static const uint32_t kProgressEventInterval = 50; // ms
+
 class HTMLInputElementState MOZ_FINAL : public nsISupports
 {
   public:
     NS_DECLARE_STATIC_IID_ACCESSOR(NS_INPUT_ELEMENT_STATE_IID)
     NS_DECL_ISUPPORTS
 
     bool IsCheckedSet() {
       return mCheckedSet;
@@ -478,28 +482,30 @@ public:
         new DirPickerRecursiveFileEnumerator(mTopDir);
       bool hasMore = true;
       nsCOMPtr<nsISupports> tmp;
       while (NS_SUCCEEDED(iter->HasMoreElements(&hasMore)) && hasMore) {
         iter->GetNext(getter_AddRefs(tmp));
         nsCOMPtr<nsIDOMFile> domFile = do_QueryInterface(tmp);
         MOZ_ASSERT(domFile);
         mFileList.AppendElement(domFile);
+        mInput->SetFileListProgress(mFileList.Length());
       }
       return NS_DispatchToMainThread(this);
     }
 
     // Now back on the main thread, set the list on our HTMLInputElement:
     if (mFileList.IsEmpty()) {
       return NS_OK;
     }
     // The text control frame (if there is one) isn't going to send a change
     // event because it will think this is done by a script.
     // So, we can safely send one by ourself.
     mInput->SetFiles(mFileList, true);
+    mInput->MaybeDispatchProgressEvent(true); // last progress event
     nsresult rv =
       nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(),
                                            static_cast<nsIDOMHTMLInputElement*>(mInput.get()),
                                            NS_LITERAL_STRING("change"), true,
                                            false);
     // Clear mInput to make sure that it can't lose its last strong ref off the
     // main thread (which may happen if our dtor runs off the main thread)!
     mInput = nullptr;
@@ -562,16 +568,19 @@ HTMLInputElement::nsFilePickerShownCallb
 
     HTMLInputElement::gUploadLastDir->StoreLastUsedDirectory(
       mInput->OwnerDoc(), pickedDir);
 
     nsCOMPtr<nsIEventTarget> target
       = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
     NS_ASSERTION(target, "Must have stream transport service");
 
+    mInput->ResetProgressCounters();
+    mInput->StartProgressEventTimer();
+
     // DirPickerBuildFileListTask takes care of calling SetFiles() and
     // dispatching the "change" event.
     nsRefPtr<DirPickerBuildFileListTask> event =
       new DirPickerBuildFileListTask(mInput.get(), pickedDir.get());
     return target->Dispatch(event, NS_DISPATCH_NORMAL);
   }
 
   // Collect new selected filenames
@@ -987,32 +996,35 @@ static nsresult FireEventForAccessibilit
 
 //
 // construction, destruction
 //
 
 HTMLInputElement::HTMLInputElement(already_AddRefed<nsINodeInfo> aNodeInfo,
                                    FromParser aFromParser)
   : nsGenericHTMLFormElementWithState(aNodeInfo)
+  , mFileListProgress(0)
+  , mLastFileListProgress(0)
   , mType(kInputDefaultType->value)
   , mDisabledChanged(false)
   , mValueChanged(false)
   , mCheckedChanged(false)
   , mChecked(false)
   , mHandlingSelectEvent(false)
   , mShouldInitChecked(false)
   , mParserCreating(aFromParser != NOT_FROM_PARSER)
   , mInInternalActivate(false)
   , mCheckedIsToggled(false)
   , mIndeterminate(false)
   , mInhibitRestoration(aFromParser & FROM_PARSER_FRAGMENT)
   , mCanShowValidUI(true)
   , mCanShowInvalidUI(true)
   , mHasRange(false)
   , mIsDraggingRange(false)
+  , mProgressTimerIsActive(false)
 {
   // We are in a type=text so we now we currenty need a nsTextEditorState.
   mInputData.mState = new nsTextEditorState(this);
 
   if (!gUploadLastDir)
     HTMLInputElement::InitUploadLastDir();
 
   // Set up our default state.  By default we're enabled (since we're
@@ -1091,24 +1103,25 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_IN
   //XXX should unlink more?
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_ADDREF_INHERITED(HTMLInputElement, Element)
 NS_IMPL_RELEASE_INHERITED(HTMLInputElement, Element)
 
 // QueryInterface implementation for HTMLInputElement
 NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLInputElement)
-  NS_INTERFACE_TABLE_INHERITED8(HTMLInputElement,
+  NS_INTERFACE_TABLE_INHERITED9(HTMLInputElement,
                                 nsIDOMHTMLInputElement,
                                 nsITextControlElement,
                                 nsIPhonetic,
                                 imgINotificationObserver,
                                 nsIImageLoadingContent,
                                 imgIOnloadBlocker,
                                 nsIDOMNSEditableElement,
+                                nsITimerCallback,
                                 nsIConstraintValidation)
 NS_INTERFACE_TABLE_TAIL_INHERITING(nsGenericHTMLFormElementWithState)
 
 // nsIConstraintValidation
 NS_IMPL_NSICONSTRAINTVALIDATION_EXCEPT_SETCUSTOMVALIDITY(HTMLInputElement)
 
 // nsIDOMNode
 
@@ -2426,16 +2439,103 @@ void
 HTMLInputElement::OpenDirectoryPicker(ErrorResult& aRv)
 {
   if (mType != NS_FORM_INPUT_FILE) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
   }
   InitFilePicker(FILE_PICKER_DIRECTORY);
 }
 
+void
+HTMLInputElement::StartProgressEventTimer()
+{
+  if (!mProgressTimer) {
+    mProgressTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+  }
+  if (mProgressTimer) {
+    mProgressTimerIsActive = true;
+    mProgressTimer->Cancel();
+    mProgressTimer->InitWithCallback(this, kProgressEventInterval,
+                                           nsITimer::TYPE_ONE_SHOT);
+  }
+}
+
+// nsITimerCallback's only method
+NS_IMETHODIMP
+HTMLInputElement::Notify(nsITimer* aTimer)
+{
+  if (mProgressTimer == aTimer) {
+    mProgressTimerIsActive = false;
+    MaybeDispatchProgressEvent(false);
+    return NS_OK;
+  }
+
+  // Just in case some JS user wants to QI to nsITimerCallback and play with us...
+  NS_WARNING("Unexpected timer!");
+  return NS_ERROR_INVALID_POINTER;
+}
+
+void
+HTMLInputElement::MaybeDispatchProgressEvent(bool aFinalProgress)
+{
+  nsRefPtr<HTMLInputElement> kungFuDeathGrip;
+
+  if (aFinalProgress && mProgressTimerIsActive) {
+    // mProgressTimer may hold the last reference to us, so take another strong
+    // ref to make sure we don't die under Cancel() and leave this method
+    // running on deleted memory.
+    kungFuDeathGrip = this;
+
+    mProgressTimerIsActive = false;
+    mProgressTimer->Cancel();
+  }
+
+  if (mProgressTimerIsActive ||
+      mFileListProgress == mLastFileListProgress) {
+    return;
+  }
+
+  if (!aFinalProgress) {
+    StartProgressEventTimer();
+  }
+
+  mLastFileListProgress = mFileListProgress;
+
+  DispatchProgressEvent(NS_LITERAL_STRING(PROGRESS_STR),
+                        false, mLastFileListProgress,
+                        0);
+}
+
+void
+HTMLInputElement::DispatchProgressEvent(const nsAString& aType,
+                                        bool aLengthComputable,
+                                        uint64_t aLoaded, uint64_t aTotal)
+{
+  NS_ASSERTION(!aType.IsEmpty(), "missing event type");
+
+  nsCOMPtr<nsIDOMEvent> event;
+  nsresult rv = NS_NewDOMProgressEvent(getter_AddRefs(event), this,
+                                       nullptr, nullptr);
+  if (NS_FAILED(rv)) {
+    return;
+  }
+
+  nsCOMPtr<nsIDOMProgressEvent> progress = do_QueryInterface(event);
+  if (!progress) {
+    return;
+  }
+
+  progress->InitProgressEvent(aType, false, false, aLengthComputable,
+                              aLoaded, (aTotal == UINT64_MAX) ? 0 : aTotal);
+
+  event->SetTrusted(true);
+
+  DispatchDOMEvent(nullptr, event, nullptr, nullptr);
+}
+
 nsresult
 HTMLInputElement::UpdateFileList()
 {
   if (mFileList) {
     mFileList->Clear();
 
     const nsTArray<nsCOMPtr<nsIDOMFile> >& files = GetFilesInternal();
     for (uint32_t i = 0; i < files.Length(); ++i) {
--- a/content/html/content/src/HTMLInputElement.h
+++ b/content/html/content/src/HTMLInputElement.h
@@ -6,16 +6,17 @@
 #ifndef mozilla_dom_HTMLInputElement_h
 #define mozilla_dom_HTMLInputElement_h
 
 #include "mozilla/Attributes.h"
 #include "nsGenericHTMLElement.h"
 #include "nsImageLoadingContent.h"
 #include "nsIDOMHTMLInputElement.h"
 #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 "nsIFilePicker.h"
 #include "nsIContentPrefService2.h"
 #include "mozilla/Decimal.h"
@@ -77,16 +78,17 @@ public:
 };
 
 class HTMLInputElement MOZ_FINAL : public nsGenericHTMLFormElementWithState,
                                    public nsImageLoadingContent,
                                    public nsIDOMHTMLInputElement,
                                    public nsITextControlElement,
                                    public nsIPhonetic,
                                    public nsIDOMNSEditableElement,
+                                   public nsITimerCallback,
                                    public nsIConstraintValidation
 {
 public:
   using nsIConstraintValidation::GetValidationMessage;
   using nsIConstraintValidation::CheckValidity;
   using nsIConstraintValidation::WillValidate;
   using nsIConstraintValidation::Validity;
   using nsGenericHTMLFormElementWithState::GetForm;
@@ -220,16 +222,24 @@ public:
   static UploadLastDir* gUploadLastDir;
   // create and destroy the static UploadLastDir object for remembering
   // which directory was last used on a site-by-site basis
   static void InitUploadLastDir();
   static void DestroyUploadLastDir();
 
   void MaybeLoadImage();
 
+  // nsITimerCallback
+  NS_DECL_NSITIMERCALLBACK
+
+  // Avoid warning about the implementation of nsITimerCallback::Notify hiding
+  // our nsImageLoadingContent base class' implementation of
+  // imgINotificationObserver::Notify:
+  using nsImageLoadingContent::Notify;
+
   // nsIConstraintValidation
   bool     IsTooLong();
   bool     IsValueMissing() const;
   bool     HasTypeMismatch() const;
   bool     HasPatternMismatch() const;
   bool     IsRangeOverflow() const;
   bool     IsRangeUnderflow() const;
   bool     HasStepMismatch() const;
@@ -387,16 +397,27 @@ public:
   }
 
   // XPCOM GetForm() is OK
 
   nsDOMFileList* GetFiles();
 
   void OpenDirectoryPicker(ErrorResult& aRv);
 
+  void ResetProgressCounters()
+  {
+    mFileListProgress = 0;
+    mLastFileListProgress = 0;
+  }
+  void StartProgressEventTimer();
+  void MaybeDispatchProgressEvent(bool aFinalProgress);
+  void DispatchProgressEvent(const nsAString& aType,
+                             bool aLengthComputable,
+                             uint64_t aLoaded, uint64_t aTotal);
+
   // XPCOM GetFormAction() is OK
   void SetFormAction(const nsAString& aValue, ErrorResult& aRv)
   {
     SetHTMLAttr(nsGkAtoms::formaction, aValue, aRv);
   }
 
   // XPCOM GetFormEnctype() is OK
   void SetFormEnctype(const nsAString& aValue, ErrorResult& aRv)
@@ -643,16 +664,23 @@ public:
   bool MozIsTextField(bool aExcludePassword);
 
   nsIEditor* GetEditor();
 
   // XPCOM SetUserInput() is OK
 
   // XPCOM GetPhonetic() is OK
 
+  void SetFileListProgress(uint32_t mFileCount)
+  {
+    MOZ_ASSERT(!NS_IsMainThread(),
+               "Why are we calling this on the main thread?");
+    mFileListProgress = mFileCount;
+  }
+
 protected:
   virtual JSObject* WrapNode(JSContext* aCx,
                              JS::Handle<JSObject*> aScope) MOZ_OVERRIDE;
 
   // Pull IsSingleLineTextControl into our scope, otherwise it'd be hidden
   // by the nsITextControlElement version.
   using nsGenericHTMLFormElementWithState::IsSingleLineTextControl;
 
@@ -1139,32 +1167,52 @@ protected:
 
   /**
    * If mIsDraggingRange is true, this is the value that the input had before
    * the drag started. Used to reset the input to its old value if the drag is
    * canceled.
    */
   Decimal mRangeThumbDragStartValue;
 
+  /**
+   * Timer that is used when mType == NS_FORM_INPUT_FILE and the user selects a
+   * directory. It is used to fire progress events while the list of files
+   * under that directory tree is built.
+   */
+  nsCOMPtr<nsITimer> mProgressTimer;
+
   // Step scale factor values, for input types that have one.
   static const Decimal kStepScaleFactorDate;
   static const Decimal kStepScaleFactorNumberRange;
   static const Decimal kStepScaleFactorTime;
 
   // Default step base value when a type do not have specific one.
   static const Decimal kDefaultStepBase;
 
   // Default step used when there is no specified step.
   static const Decimal kDefaultStep;
   static const Decimal kDefaultStepTime;
 
   // Float value returned by GetStep() when the step attribute is set to 'any'.
   static const Decimal kStepAny;
 
   /**
+   * The number of files added to the FileList being built off-main-thread when
+   * mType == NS_FORM_INPUT_FILE and the user selects a directory. This is set
+   * off the main thread, read on main thread.
+   */
+  mozilla::Atomic<uint32_t> mFileListProgress;
+
+  /**
+   * The number of files added to the FileList at the time the last progress
+   * event was fired.
+   */
+  uint32_t mLastFileListProgress;
+
+  /**
    * The type of this input (<input type=...>) as an integer.
    * @see nsIFormControl.h (specifically NS_FORM_INPUT_*)
    */
   uint8_t                  mType;
   bool                     mDisabledChanged     : 1;
   bool                     mValueChanged        : 1;
   bool                     mCheckedChanged      : 1;
   bool                     mChecked             : 1;
@@ -1174,16 +1222,17 @@ protected:
   bool                     mInInternalActivate  : 1;
   bool                     mCheckedIsToggled    : 1;
   bool                     mIndeterminate       : 1;
   bool                     mInhibitRestoration  : 1;
   bool                     mCanShowValidUI      : 1;
   bool                     mCanShowInvalidUI    : 1;
   bool                     mHasRange            : 1;
   bool                     mIsDraggingRange     : 1;
+  bool                     mProgressTimerIsActive : 1;
 
 private:
 
   /**
    * Returns true if this input's type will fire a DOM "change" event when it
    * loses focus if its value has changed since it gained focus.
    */
   bool MayFireChangeOnBlur() const {