Bug 391847. Coalesce accessible mutation events for the same subtree. r=ginn.chen, sr=bz, a=bz
authoraaronleventhal@moonset.net
Tue, 14 Aug 2007 11:47:49 -0700
changeset 4637 ce00d2775d02e60510068a476e775ceb54a605e6
parent 4636 6b0556ae518fff01781d9dd9e420ba7029df7e18
child 4638 48e4a93b23833f3763f5c74849d55e1d219144f6
push idunknown
push userunknown
push dateunknown
reviewersginn.chen, bz, bz
bugs391847
milestone1.9a8pre
Bug 391847. Coalesce accessible mutation events for the same subtree. r=ginn.chen, sr=bz, a=bz
accessible/src/base/nsAccessibilityUtils.cpp
accessible/src/base/nsAccessibilityUtils.h
accessible/src/base/nsCaretAccessible.cpp
accessible/src/base/nsDocAccessible.cpp
accessible/src/base/nsDocAccessible.h
accessible/src/base/nsRootAccessible.cpp
layout/base/nsFrameManager.cpp
--- a/accessible/src/base/nsAccessibilityUtils.cpp
+++ b/accessible/src/base/nsAccessibilityUtils.cpp
@@ -178,8 +178,27 @@ nsAccUtils::FireAccEvent(PRUint32 aEvent
 
   nsCOMPtr<nsIAccessibleEvent> event =
     new nsAccEvent(aEventType, aAccessible, nsnull, aIsAsynch);
   NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY);
 
   return pAccessible->FireAccessibleEvent(event);
 }
 
+PRBool
+nsAccUtils::IsAncestorOf(nsIDOMNode *aPossibleAncestorNode,
+                         nsIDOMNode *aPossibleDescendantNode)
+{
+  NS_ENSURE_ARG_POINTER(aPossibleAncestorNode);
+  NS_ENSURE_ARG_POINTER(aPossibleDescendantNode);
+
+  nsCOMPtr<nsIDOMNode> loopNode = aPossibleDescendantNode;
+  nsCOMPtr<nsIDOMNode> parentNode;
+  while (NS_SUCCEEDED(loopNode->GetParentNode(getter_AddRefs(parentNode))) &&
+         parentNode) {
+    if (parentNode == aPossibleAncestorNode) {
+      return PR_TRUE;
+    }
+    loopNode.swap(parentNode);
+  }
+  return PR_FALSE;
+}
+
--- a/accessible/src/base/nsAccessibilityUtils.h
+++ b/accessible/src/base/nsAccessibilityUtils.h
@@ -108,11 +108,21 @@ public:
    */
   static PRBool HasListener(nsIContent *aContent, const nsAString& aEventType);
 
   /**
    * Fire accessible event of the given type for the given accessible.
    */
   static nsresult FireAccEvent(PRUint32 aEventType, nsIAccessible *aAccessible,
                                PRBool aIsAsynch = PR_FALSE);
+
+  /**
+   * Is the first passed in node an ancestor of the second?
+   * Note: A node is not considered to be the ancestor of itself.
+   * @aPossibleAncestorNode -- node to test for ancestor-ness of aPossibleDescendantNode
+   * @aPossibleDescendantNode -- node to test for descendant-ness of aPossibleAncestorNode
+   * @return PR_TRUE if aPossibleAncestorNode is an ancestor of aPossibleDescendantNode
+   */
+  static PRBool IsAncestorOf(nsIDOMNode *aPossibleAncestorNode,
+                             nsIDOMNode *aPossibleDescendantNode);
 };
 
 #endif
--- a/accessible/src/base/nsCaretAccessible.cpp
+++ b/accessible/src/base/nsCaretAccessible.cpp
@@ -216,17 +216,17 @@ NS_IMETHODIMP nsCaretAccessible::NotifyS
   }
   mLastCaretOffset = caretOffset;
   mLastTextAccessible = textAcc;
 
   nsCOMPtr<nsIAccessibleCaretMoveEvent> event =
     new nsAccCaretMoveEvent(focusNode);
   NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY);
 
-  return mRootAccessible->FireDelayedAccessibleEvent(event, PR_FALSE);
+  return mRootAccessible->FireDelayedAccessibleEvent(event, nsDocAccessible::eRemoveDupes);
 }
 
 nsRect
 nsCaretAccessible::GetCaretRect(nsIWidget **aOutWidget)
 {
   nsRect caretRect;
   NS_ENSURE_TRUE(aOutWidget, caretRect);
   *aOutWidget = nsnull;
--- a/accessible/src/base/nsDocAccessible.cpp
+++ b/accessible/src/base/nsDocAccessible.cpp
@@ -1001,17 +1001,17 @@ nsDocAccessible::AttributeChanged(nsIDoc
       // Need to find the right event to use here, SELECTION_WITHIN would
       // seem right but we had started using it for something else
       nsCOMPtr<nsIAccessNode> multiSelectAccessNode =
         do_QueryInterface(multiSelect);
       nsCOMPtr<nsIDOMNode> multiSelectDOMNode;
       multiSelectAccessNode->GetDOMNode(getter_AddRefs(multiSelectDOMNode));
       NS_ASSERTION(multiSelectDOMNode, "A new accessible without a DOM node!");
       FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_SELECTION_WITHIN,
-                              multiSelectDOMNode, nsnull, PR_TRUE);
+                              multiSelectDOMNode, nsnull, eAllowDupes);
 
       static nsIContent::AttrValuesArray strings[] =
         {&nsAccessibilityAtoms::_empty, &nsAccessibilityAtoms::_false, nsnull};
       if (aContent->FindAttrValueIn(kNameSpaceID_None,
                                     nsAccessibilityAtoms::selected,
                                     strings, eCaseMatters) !=
           nsIContent::ATTR_VALUE_NO_MATCH) {
 
@@ -1376,30 +1376,30 @@ nsDocAccessible::FireTextChangedEventOnD
     return;
 
   textAccessible->FireAccessibleEvent(event);
 }
 
 nsresult nsDocAccessible::FireDelayedToolkitEvent(PRUint32 aEvent,
                                                   nsIDOMNode *aDOMNode,
                                                   void *aData,
-                                                  PRBool aAllowDupes,
+                                                  EDupeEventRule aAllowDupes,
                                                   PRBool aIsAsynch)
 {
   nsCOMPtr<nsIAccessibleEvent> event =
     new nsAccEvent(aEvent, aDOMNode, aData, PR_TRUE);
   NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY);
 
   return FireDelayedAccessibleEvent(event, aAllowDupes, aIsAsynch);
 }
 
 nsresult
 nsDocAccessible::FireDelayedAccessibleEvent(nsIAccessibleEvent *aEvent,
-                                           PRBool aAllowDupes,
-                                           PRBool aIsAsynch)
+                                            EDupeEventRule aAllowDupes,
+                                            PRBool aIsAsynch)
 {
   PRBool isTimerStarted = PR_TRUE;
   PRInt32 numQueuedEvents = mEventsToFire.Count();
   if (!mFireEventTimer) {
     // Do not yet have a timer going for firing another event.
     mFireEventTimer = do_CreateInstance("@mozilla.org/timer;1");
     NS_ENSURE_TRUE(mFireEventTimer, NS_ERROR_OUT_OF_MEMORY);
   }
@@ -1414,17 +1414,46 @@ nsDocAccessible::FireDelayedAccessibleEv
     // If already asynchronous don't call PrepareFromEvent() -- it
     // should only be called while ESM still knows if the event occurred
     // originally because of user input
     nsAccEvent::PrepareForEvent(newEventDOMNode);
   }
 
   if (numQueuedEvents == 0) {
     isTimerStarted = PR_FALSE;
-  } else if (!aAllowDupes) {
+  } else if (aAllowDupes == eCoalesceFromSameSubtree) {
+    // Especially for mutation events, we will define a duplicate event
+    // as one on the same node or on a descendant node.
+    // This prevents a flood of events when a subtree is changed.
+    for (PRInt32 index = 0; index < numQueuedEvents; index ++) {
+      nsIAccessibleEvent *accessibleEvent = mEventsToFire[index];
+      NS_ASSERTION(accessibleEvent, "Array item is not an accessible event");
+      if (!accessibleEvent) {
+        continue;
+      }
+      PRUint32 eventType;
+      accessibleEvent->GetEventType(&eventType);
+      if (eventType == newEventType) {
+        nsCOMPtr<nsIDOMNode> domNode;
+        accessibleEvent->GetDOMNode(getter_AddRefs(domNode));
+        if (newEventDOMNode == domNode || nsAccUtils::IsAncestorOf(newEventDOMNode, domNode)) {
+          mEventsToFire.RemoveObjectAt(index);
+          // The other event is the same type, but in a descendant of this
+          // event, so remove that one. The umbrella event in the ancestor
+          // is already enough
+          -- index;
+          -- numQueuedEvents;
+        }
+        else if (nsAccUtils::IsAncestorOf(domNode, newEventDOMNode)) {
+          // There is a better SHOW/HIDE event (it's in an ancestor)
+          return NS_OK;
+        }
+      }    
+    }
+  } else if (aAllowDupes == eRemoveDupes) {
     // Check for repeat events. If a redundant event exists remove
     // original and put the new event at the end of the queue
     // so it is fired after the others
     for (PRInt32 index = 0; index < numQueuedEvents; index ++) {
       nsIAccessibleEvent *accessibleEvent = mEventsToFire[index];
       NS_ASSERTION(accessibleEvent, "Array item is not an accessible event");
       if (!accessibleEvent) {
         continue;
@@ -1501,16 +1530,26 @@ NS_IMETHODIMP nsDocAccessible::FlushPend
           }
         } 
       }
       else {
         // The input state was previously stored with the nsIAccessibleEvent,
         // so use that state now when firing the event
         nsAccEvent::PrepareForEvent(accessibleEvent);
         FireAccessibleEvent(accessibleEvent);
+        // Post event processing
+        if (eventType == nsIAccessibleEvent::EVENT_ASYNCH_HIDE ||
+            eventType == nsIAccessibleEvent::EVENT_DOM_DESTROY) {
+          // Shutdown nsIAccessNode's or nsIAccessibles for any DOM nodes in this subtree
+          nsCOMPtr<nsIDOMNode> hidingNode;
+          accessibleEvent->GetDOMNode(getter_AddRefs(hidingNode));
+          if (hidingNode) {
+            RefreshNodes(hidingNode); // Will this bite us with asynch events
+          }
+        }
       }
     }
   }
   mEventsToFire.Clear(); // Clear out array
   return NS_OK;
 }
 
 void nsDocAccessible::FlushEventsCallback(nsITimer *aTimer, void *aClosure)
@@ -1632,17 +1671,17 @@ NS_IMETHODIMP nsDocAccessible::Invalidat
     // which it is if anyone asks for its children right now.
     return InvalidateChildren();
   }
 
   // Update last change state information
   nsCOMPtr<nsIAccessNode> childAccessNode;
   GetCachedAccessNode(childNode, getter_AddRefs(childAccessNode));
   nsCOMPtr<nsIAccessible> childAccessible = do_QueryInterface(childAccessNode);
-  if (!childAccessible && isHiding) {
+  if (!childAccessible && !isHiding) {
     // If not about to hide it, make sure there's an accessible so we can fire an
     // event for it
     GetAccService()->GetAccessibleFor(childNode, getter_AddRefs(childAccessible));
   }
 
 #ifdef DEBUG_A11Y
   nsAutoString localName;
   childNode->GetLocalName(localName);
@@ -1663,24 +1702,26 @@ NS_IMETHODIMP nsDocAccessible::Invalidat
     printf("[Destroy  %s %s]\n", NS_ConvertUTF16toUTF8(localName).get(), hasAccessible);
   }
   else if (aChangeEventType == nsIAccessibleEvent::EVENT_DOM_SIGNIFICANT_CHANGE) {
     printf("[Type change %s %s]\n", NS_ConvertUTF16toUTF8(localName).get(), hasAccessible);
   }
 #endif
 
   if (!isShowing) {
-    // Fire EVENT_HIDE or EVENT_DOM_DESTROY if previous accessible existed for node being hidden.
+    // Fire EVENT_ASYNCH_HIDE or EVENT_DOM_DESTROY if previous accessible existed for node being hidden.
     // Fire this before the accessible goes away.
     if (childAccessible) {
-      PRUint32 removalEvent = isAsynch ? nsIAccessibleEvent::EVENT_ASYNCH_HIDE : nsIAccessibleEvent::EVENT_DOM_DESTROY;
-      nsAccUtils::FireAccEvent(removalEvent, childAccessible, isAsynch);
+      PRUint32 removalEventType = isAsynch ? nsIAccessibleEvent::EVENT_ASYNCH_HIDE :
+                                  nsIAccessibleEvent::EVENT_DOM_DESTROY;
+      nsCOMPtr<nsIAccessibleEvent> removalEvent =
+        new nsAccEvent(removalEventType, childAccessible, nsnull, PR_TRUE);
+      NS_ENSURE_TRUE(removalEvent, NS_ERROR_OUT_OF_MEMORY);
+      FireDelayedAccessibleEvent(removalEvent, eCoalesceFromSameSubtree, isAsynch);
     }
-    // Shutdown nsIAccessNode's or nsIAccessibles for any DOM nodes in this subtree
-    RefreshNodes(childNode);
   }
 
   // We need to get an accessible for the mutation event's container node
   // If there is no accessible for that node, we need to keep moving up the parent
   // chain so there is some accessible.
   // We will use this accessible to fire the accessible mutation event.
   // We're guaranteed success, because we will eventually end up at the doc accessible,
   // and there is always one of those.
@@ -1699,37 +1740,35 @@ NS_IMETHODIMP nsDocAccessible::Invalidat
   if (aChild && !isHiding) {
     // Fire EVENT_SHOW, EVENT_MENUPOPUP_START for newly visible content.
     // Fire after a short timer, because we want to make sure the view has been
     // updated to make this accessible content visible. If we don't wait,
     // the assistive technology may receive the event and then retrieve
     // nsIAccessibleStates::STATE_INVISIBLE for the event's accessible object.
     PRUint32 additionEvent = isAsynch ? nsIAccessibleEvent::EVENT_ASYNCH_SHOW :
                                         nsIAccessibleEvent::EVENT_DOM_CREATE;
-    if (!isAsynch) {
-      // Calculate "is from user input" while we still synchronous and have the info
-      nsAccEvent::PrepareForEvent(childNode);
-    }
-    FireDelayedToolkitEvent(additionEvent, childNode, nsnull, PR_TRUE, isAsynch);
+    FireDelayedToolkitEvent(additionEvent, childNode, nsnull,
+                            eCoalesceFromSameSubtree, isAsynch);
 
     // Check to see change occured in an ARIA menu, and fire an EVENT_MENUPOPUP_START if it did
     nsAutoString role;
     if (GetRoleAttribute(aChild, role) &&
         StringEndsWith(role, NS_LITERAL_STRING(":menu"), nsCaseInsensitiveStringComparator())) {
       FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START,
-                              childNode, nsnull, PR_TRUE, isAsynch);
+                              childNode, nsnull, eAllowDupes, isAsynch);
     }
 
     // Check to see if change occured inside an alert, and fire an EVENT_ALERT if it did
     nsIContent *ancestor = aChild;
     while (ancestor) {
       if (GetRoleAttribute(ancestor, role) &&
           StringEndsWith(role, NS_LITERAL_STRING(":alert"), nsCaseInsensitiveStringComparator())) {
         nsCOMPtr<nsIDOMNode> alertNode(do_QueryInterface(ancestor));
-        FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_ALERT, alertNode, nsnull, PR_FALSE, isAsynch);
+        FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_ALERT, alertNode, nsnull,
+                                eRemoveDupes, isAsynch);
         break;
       }
       ancestor = ancestor->GetParent();
     }
   }
 
   return NS_OK;
 }
--- a/accessible/src/base/nsDocAccessible.h
+++ b/accessible/src/base/nsDocAccessible.h
@@ -97,44 +97,49 @@ class nsDocAccessible : public nsHyperTe
     NS_IMETHOD Init();
 
     // nsPIAccessNode
     NS_IMETHOD_(nsIFrame *) GetFrame(void);
 
     // nsIAccessibleText
     NS_IMETHOD GetAssociatedEditor(nsIEditor **aEditor);
 
+    enum EDupeEventRule { eAllowDupes, eCoalesceFromSameSubtree, eRemoveDupes };
+
     /**
       * Non-virtual method to fire a delayed event after a 0 length timeout
       *
       * @param aEvent - the nsIAccessibleEvent event ype
       * @param aDOMNode - DOM node the accesible event should be fired for
       * @param aData - any additional data for the event
-      * @param aAllowDupes - set to PR_TRUE if more than one event of the same
-      *                      type is allowed. By default this is false and events
-      *                      of the same type are discarded (the last one is used)
+      * @param aAllowDupes - eAllowDupes: more than one event of the same type is allowed. 
+      *                      eCoalesceFromSameSubtree: if two events are in the same subtree,
+      *                                                only the event on ancestor is used
+      *                      eRemoveDupes (default): events of the same type are discarded
+      *                                              (the last one is used)
+      *
       * @param aIsAsyn - set to PR_TRUE if this is not being called from code
       *                  synchronous with a DOM event
       */
     nsresult FireDelayedToolkitEvent(PRUint32 aEvent, nsIDOMNode *aDOMNode,
-                                     void *aData, PRBool aAllowDupes = PR_FALSE,
+                                     void *aData, EDupeEventRule aAllowDupes = eRemoveDupes,
                                      PRBool aIsAsynch = PR_FALSE);
 
     /**
      * Fire accessible event in timeout.
      *
      * @param aEvent - the event to fire
      * @param aAllowDupes - if false then delayed events of the same type and
      *                      for the same DOM node in the event queue won't
      *                      be fired.
      * @param aIsAsych - set to PR_TRUE if this is being called from
      *                   an event asynchronous with the DOM
      */
     nsresult FireDelayedAccessibleEvent(nsIAccessibleEvent *aEvent,
-                                        PRBool aAllowDupes = PR_FALSE,
+                                        EDupeEventRule aAllowDupes = eRemoveDupes,
                                         PRBool aIsAsynch = PR_FALSE);
 
     void ShutdownChildDocuments(nsIDocShellTreeItem *aStart);
 
   protected:
     virtual void GetBoundsRect(nsRect& aRect, nsIFrame** aRelativeFrame);
     virtual nsresult AddEventListeners();
     virtual nsresult RemoveEventListeners();
--- a/accessible/src/base/nsRootAccessible.cpp
+++ b/accessible/src/base/nsRootAccessible.cpp
@@ -414,17 +414,17 @@ void nsRootAccessible::TryFireEarlyLoadE
     if (state & nsIAccessibleStates::STATE_BUSY) {
       // Don't fire page load events on subdocuments for initial page load of entire page
       return;
     }
   }
 
   // No frames or iframes, so we can fire the doc load finished event early
   FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_INTERNAL_LOAD, aDocNode,
-                          nsnull, PR_FALSE);
+                          nsnull, eRemoveDupes);
 }
 
 PRBool nsRootAccessible::FireAccessibleFocusEvent(nsIAccessible *aAccessible,
                                                   nsIDOMNode *aNode,
                                                   nsIDOMEvent *aFocusEvent,
                                                   PRBool aForceEvent,
                                                   PRBool aIsAsynch)
 {
@@ -506,17 +506,17 @@ PRBool nsRootAccessible::FireAccessibleF
       // Suppress document focus, because real DOM focus will be fired next,
       // and that's what we care about
       // Make sure we never fire focus for the nsRootAccessible (mDOMNode)
       return PR_FALSE;
     }
   }
 
   FireDelayedToolkitEvent(nsIAccessibleEvent::EVENT_FOCUS,
-                          finalFocusNode, nsnull, PR_FALSE, aIsAsynch);
+                          finalFocusNode, nsnull, eRemoveDupes, aIsAsynch);
 
   return PR_TRUE;
 }
 
 void nsRootAccessible::FireCurrentFocusEvent()
 {
   nsCOMPtr<nsIDOMNode> focusedNode = GetCurrentFocus();
   if (!focusedNode) {
--- a/layout/base/nsFrameManager.cpp
+++ b/layout/base/nsFrameManager.cpp
@@ -1367,17 +1367,18 @@ nsFrameManager::ReResolveStyleContext(ns
       // XXX need to do overflow frames???
     }
 
     newContext->Release();
   }
 
 #ifdef ACCESSIBILITY
   if (isAccessibilityActive &&
-      aFrame->GetStyleVisibility()->IsVisible() != isVisible) {
+      aFrame->GetStyleVisibility()->IsVisible() != isVisible &&
+      !aFrame->GetPrevContinuation()) { // Primary frames only
     // XXX Visibility does not affect descendents with visibility set
     // Work on a separate, accurate mechanism for dealing with visibility changes.
     // A significant enough change occured that this part
     // of the accessible tree is no longer valid.
     nsCOMPtr<nsIAccessibilityService> accService = 
       do_GetService("@mozilla.org/accessibilityService;1");
     if (accService) {
       accService->InvalidateSubtreeFor(mPresShell, aFrame->GetContent(),