Bug 574003 - coalesce text evetns on nodes removal, r=davidb, marcoz

This commit is contained in:
Alexander Surkov 2010-07-02 10:49:42 +09:00
parent 344791f1a9
commit a90254e830
8 changed files with 252 additions and 51 deletions

View File

@ -300,6 +300,7 @@ nsAccReorderEvent::HasAccessibleInReasonSubtree()
return accessible || nsAccUtils::HasAccessibleChildren(mReasonNode);
}
////////////////////////////////////////////////////////////////////////////////
// nsAccStateChangeEvent
////////////////////////////////////////////////////////////////////////////////
@ -387,14 +388,12 @@ NS_IMPL_ISUPPORTS_INHERITED1(nsAccTextChangeEvent, nsAccEvent,
// a defunct accessible so the behaviour should be equivalent.
// XXX revisit this when coalescence is faster (eCoalesceFromSameSubtree)
nsAccTextChangeEvent::
nsAccTextChangeEvent(nsAccessible *aAccessible,
PRInt32 aStart, PRUint32 aLength,
nsAccTextChangeEvent(nsAccessible *aAccessible, PRInt32 aStart,
nsAString& aModifiedText, PRBool aIsInserted,
PRBool aIsAsynch, EIsFromUserInput aIsFromUserInput) :
nsAccEvent(aIsInserted ? nsIAccessibleEvent::EVENT_TEXT_INSERTED : nsIAccessibleEvent::EVENT_TEXT_REMOVED,
aAccessible, aIsAsynch, aIsFromUserInput, eAllowDupes),
mStart(aStart), mLength(aLength), mIsInserted(aIsInserted),
mModifiedText(aModifiedText)
mStart(aStart), mIsInserted(aIsInserted), mModifiedText(aModifiedText)
{
}
@ -410,7 +409,7 @@ NS_IMETHODIMP
nsAccTextChangeEvent::GetLength(PRUint32 *aLength)
{
NS_ENSURE_ARG_POINTER(aLength);
*aLength = mLength;
*aLength = GetLength();
return NS_OK;
}
@ -429,6 +428,24 @@ nsAccTextChangeEvent::GetModifiedText(nsAString& aModifiedText)
return NS_OK;
}
////////////////////////////////////////////////////////////////////////////////
// AccHideEvent
////////////////////////////////////////////////////////////////////////////////
AccHideEvent::
AccHideEvent(nsAccessible* aTarget, nsINode* aTargetNode,
PRBool aIsAsynch, EIsFromUserInput aIsFromUserInput) :
nsAccEvent(nsIAccessibleEvent::EVENT_HIDE, aTarget, aIsAsynch,
aIsFromUserInput, eCoalesceFromSameSubtree)
{
mNode = aTargetNode;
mParent = mAccessible->GetCachedParent();
mNextSibling = mAccessible->GetCachedNextSibling();
mPrevSibling = mAccessible->GetCachedPrevSibling();
}
////////////////////////////////////////////////////////////////////////////////
// nsAccCaretMoveEvent
////////////////////////////////////////////////////////////////////////////////

View File

@ -112,14 +112,15 @@ public:
PRBool IsFromUserInput() const { return mIsFromUserInput; }
nsAccessible *GetAccessible();
nsINode* GetNode();
nsDocAccessible* GetDocAccessible();
nsINode* GetNode();
enum EventGroup {
eGenericEvent,
eReorderEvent,
eStateChangeEvent,
eTextChangeEvent,
eHideEvent,
eCaretMoveEvent,
eTableChangeEvent
};
@ -236,7 +237,7 @@ class nsAccTextChangeEvent: public nsAccEvent,
{
public:
nsAccTextChangeEvent(nsAccessible *aAccessible, PRInt32 aStart,
PRUint32 aLength, nsAString& aModifiedText,
nsAString& aModifiedText,
PRBool aIsInserted, PRBool aIsAsynch = PR_FALSE,
EIsFromUserInput aIsFromUserInput = eAutoDetect);
@ -252,14 +253,41 @@ public:
// nsAccTextChangeEvent
PRInt32 GetStartOffset() const { return mStart; }
PRUint32 GetLength() const { return mLength; }
PRUint32 GetLength() const { return mModifiedText.Length(); }
PRBool IsTextInserted() const { return mIsInserted; }
private:
PRInt32 mStart;
PRUint32 mLength;
PRBool mIsInserted;
nsString mModifiedText;
friend class nsAccEventQueue;
};
/**
* Accessible hide events.
*/
class AccHideEvent : public nsAccEvent
{
public:
AccHideEvent(nsAccessible* aTarget, nsINode* aTargetNode,
PRBool aIsAsynch, EIsFromUserInput aIsFromUserInput);
// nsAccEvent
static const EventGroup kEventGroup = eHideEvent;
virtual unsigned int GetEventGroups() const
{
return nsAccEvent::GetEventGroups() | (1U << eHideEvent);
}
protected:
nsRefPtr<nsAccessible> mParent;
nsRefPtr<nsAccessible> mNextSibling;
nsRefPtr<nsAccessible> mPrevSibling;
nsRefPtr<nsAccTextChangeEvent> mTextChangeEvent;
friend class nsAccEventQueue;
};

View File

@ -259,8 +259,17 @@ public:
* Return cached accessible of parent-child relatives.
*/
nsAccessible* GetCachedParent() const { return mParent; }
nsAccessible* GetCachedNextSibling() const
{
return mParent ?
mParent->mChildren.SafeElementAt(mIndexInParent + 1, nsnull) : nsnull;
}
nsAccessible* GetCachedPrevSibling() const
{
return mParent ?
mParent->mChildren.SafeElementAt(mIndexInParent - 1, nsnull) : nsnull;
}
PRUint32 GetCachedChildCount() const { return mChildren.Length(); }
PRBool AreChildrenCached() const { return mAreChildrenInitialized; }
#ifdef DEBUG

View File

@ -1344,17 +1344,15 @@ nsDocAccessible::FireTextChangeEventForText(nsIContent *aContent,
if (NS_FAILED(rv))
return;
// Get text length.
PRUint32 length = text.Length();
if (length == 0)
if (text.IsEmpty())
return;
// Normally we only fire delayed events created from the node, not an
// accessible object. See the nsAccTextChangeEvent constructor for details
// about this exceptional case.
nsRefPtr<nsAccEvent> event =
new nsAccTextChangeEvent(textAccessible, offset, length, text,
aIsInserted, PR_FALSE);
new nsAccTextChangeEvent(textAccessible, offset, text, aIsInserted,
PR_FALSE);
FireDelayedAccessibleEvent(event);
FireValueChangeForTextFields(textAccessible);
@ -1425,13 +1423,12 @@ nsDocAccessible::CreateTextChangeEventForNode(nsAccessible *aContainerAccessible
aAccessibleForChangeNode->AppendTextTo(text, 0, PR_UINT32_MAX);
}
PRUint32 length = text.Length();
if (length == 0)
if (text.IsEmpty())
return nsnull;
nsAccEvent *event =
new nsAccTextChangeEvent(aContainerAccessible, offset, length, text,
aIsInserting, aIsAsynch, aIsFromUserInput);
new nsAccTextChangeEvent(aContainerAccessible, offset, text,
aIsInserting, aIsAsynch, aIsFromUserInput);
NS_IF_ADDREF(event);
return event;
@ -1494,7 +1491,7 @@ nsDocAccessible::ProcessPendingEvent(nsAccEvent *aEvent)
// Don't need to invalidate this current accessible, but can
// just invalidate the children instead
FireShowHideEvents(node, PR_TRUE, eventType, eNormalEvent,
isAsync, isFromUserInput);
isAsync, isFromUserInput);
return;
}
gLastFocusedFrameType = newFrameType;
@ -1828,24 +1825,6 @@ nsDocAccessible::InvalidateCacheSubtree(nsIContent *aChild,
eDelayedEvent, isAsynch);
if (NS_FAILED(rv))
return;
if (aChild) {
// Fire text change unless the node being removed is for this doc.
// When a node is hidden or removed, the text in an ancestor hyper text will lose characters
// At this point we still have the frame and accessible for this node if there was one
// XXX Collate events when a range is deleted
// XXX We need a way to ignore SplitNode and JoinNode() when they
// do not affect the text within the hypertext
// Normally we only fire delayed events created from the node, not an
// accessible object. See the nsAccTextChangeEvent constructor for details
// about this exceptional case.
nsRefPtr<nsAccEvent> textChangeEvent =
CreateTextChangeEventForNode(containerAccessible, aChild, childAccessible,
PR_FALSE, isAsynch);
if (textChangeEvent) {
FireDelayedAccessibleEvent(textChangeEvent);
}
}
}
// We need to get an accessible for the mutation event's container node
@ -1948,9 +1927,19 @@ nsDocAccessible::FireShowHideEvents(nsINode *aNode,
if (accessible) {
// Found an accessible, so fire the show/hide on it and don't look further
// into this subtree.
nsRefPtr<nsAccEvent> event =
new nsAccEvent(aEventType, accessible, aIsAsyncChange, aIsFromUserInput,
nsAccEvent::eCoalesceFromSameSubtree);
nsRefPtr<nsAccEvent> event;
if (aDelayedOrNormal == eDelayedEvent &&
aEventType == nsIAccessibleEvent::EVENT_HIDE) {
// Use AccHideEvent for delayed hide events to coalesce text change events
// caused by these hide events.
event = new AccHideEvent(accessible, accessible->GetNode(),
aIsAsyncChange, aIsFromUserInput);
} else {
event = new nsAccEvent(aEventType, accessible, aIsAsyncChange,
aIsFromUserInput,
nsAccEvent::eCoalesceFromSameSubtree);
}
NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY);
if (aDelayedOrNormal == eDelayedEvent)

View File

@ -146,10 +146,16 @@ void
nsAccEventQueue::Push(nsAccEvent *aEvent)
{
mEvents.AppendElement(aEvent);
// Filter events.
CoalesceEvents();
// Associate text change with hide event if it wasn't stolen from hiding
// siblings during coalescence.
AccHideEvent* hideEvent = downcast_accEvent(aEvent);
if (hideEvent && !hideEvent->mTextChangeEvent)
CreateTextChangeEventFor(hideEvent);
// Process events.
PrepareFlush();
}
@ -205,9 +211,16 @@ nsAccEventQueue::WillRefresh(mozilla::TimeStamp aTime)
for (PRUint32 index = 0; index < length; index ++) {
nsAccEvent *accEvent = events[index];
if (accEvent->mEventRule != nsAccEvent::eDoNotEmit)
if (accEvent->mEventRule != nsAccEvent::eDoNotEmit) {
mDocument->ProcessPendingEvent(accEvent);
AccHideEvent* hideEvent = downcast_accEvent(accEvent);
if (hideEvent) {
if (hideEvent->mTextChangeEvent)
mDocument->ProcessPendingEvent(hideEvent->mTextChangeEvent);
}
}
// No document means it was shut down during event handling by AT
if (!mDocument)
return;
@ -244,15 +257,41 @@ nsAccEventQueue::CoalesceEvents()
continue; // Different type
// Skip event for application accessible since no coalescence for it
// is supported. Ignore events unattached from DOM and events from
// different documents since we can't coalesce them.
if (!thisEvent->mNode || !thisEvent->mNode->IsInDoc() ||
// is supported. Ignore events from different documents since we don't
// coalesce them.
if (!thisEvent->mNode ||
thisEvent->mNode->GetOwnerDoc() != tailEvent->mNode->GetOwnerDoc())
continue;
// If event queue contains an event of the same type and having target
// that is sibling of target of newly appended event then apply its
// event rule to the newly appended event.
// XXX: deal with show events separately because they can't be
// coalesced by accessible tree the same as hide events since target
// accessibles can't be created at this point because of lazy frame
// construction (bug 570275).
// Coalesce hide events for sibling targets.
if (tailEvent->mEventType == nsIAccessibleEvent::EVENT_HIDE) {
AccHideEvent* tailHideEvent = downcast_accEvent(tailEvent);
AccHideEvent* thisHideEvent = downcast_accEvent(thisEvent);
if (thisHideEvent->mParent == tailHideEvent->mParent) {
tailEvent->mEventRule = thisEvent->mEventRule;
// Coalesce text change events for hide events.
if (tailEvent->mEventRule != nsAccEvent::eDoNotEmit)
CoalesceTextChangeEventsFor(tailHideEvent, thisHideEvent);
return;
}
}
// Ignore events unattached from DOM since we don't coalesce them.
if (!thisEvent->mNode->IsInDoc())
continue;
// Coalesce show and reorder events by sibling targets.
if (thisEvent->mNode->GetNodeParent() ==
tailEvent->mNode->GetNodeParent()) {
tailEvent->mEventRule = thisEvent->mEventRule;
@ -462,3 +501,67 @@ nsAccEventQueue::CoalesceReorderEventsFromSameTree(nsAccEvent *aAccEvent,
if (reorderEvent->IsUnconditionalEvent())
aDescendantAccEvent->mEventRule = nsAccEvent::eDoNotEmit;
}
void
nsAccEventQueue::CoalesceTextChangeEventsFor(AccHideEvent* aTailEvent,
AccHideEvent* aThisEvent)
{
// XXX: we need a way to ignore SplitNode and JoinNode() when they do not
// affect the text within the hypertext.
nsAccTextChangeEvent* textEvent = aThisEvent->mTextChangeEvent;
if (!textEvent)
return;
if (aThisEvent->mNextSibling == aTailEvent->mAccessible) {
aTailEvent->mAccessible->AppendTextTo(textEvent->mModifiedText,
0, PR_UINT32_MAX);
} else if (aThisEvent->mPrevSibling == aTailEvent->mAccessible) {
PRUint32 oldLen = textEvent->GetLength();
aTailEvent->mAccessible->AppendTextTo(textEvent->mModifiedText,
0, PR_UINT32_MAX);
textEvent->mStart -= textEvent->GetLength() - oldLen;
}
aTailEvent->mTextChangeEvent.swap(aThisEvent->mTextChangeEvent);
}
void
nsAccEventQueue::CreateTextChangeEventFor(AccHideEvent* aEvent)
{
nsRefPtr<nsHyperTextAccessible> textAccessible = do_QueryObject(
GetAccService()->GetContainerAccessible(aEvent->mNode,
aEvent->mAccessible->GetWeakShell()));
if (!textAccessible)
return;
PRInt32 offset = 0;
nsAccessible *changeAcc =
textAccessible->DOMPointToHypertextOffset(aEvent->mNode, -1, &offset);
NS_ASSERTION(!changeAcc || changeAcc == aEvent->mAccessible,
"Hypertext is reporting a different accessible for this node");
// Don't fire event for the first html:br in an editor.
if (nsAccUtils::Role(aEvent->mAccessible) ==
nsIAccessibleRole::ROLE_WHITESPACE) {
nsCOMPtr<nsIEditor> editor;
textAccessible->GetAssociatedEditor(getter_AddRefs(editor));
if (editor) {
PRBool isEmpty = PR_FALSE;
editor->GetDocumentIsEmpty(&isEmpty);
if (isEmpty)
return;
}
}
nsAutoString text;
aEvent->mAccessible->AppendTextTo(text, 0, PR_UINT32_MAX);
if (text.IsEmpty())
return;
aEvent->mTextChangeEvent =
new nsAccTextChangeEvent(textAccessible, offset, text, PR_FALSE,
aEvent->mIsAsync,
aEvent->mIsFromUserInput ? eFromUserInput : eNoUserInput);
}

View File

@ -157,6 +157,20 @@ private:
void CoalesceReorderEventsFromSameTree(nsAccEvent *aAccEvent,
nsAccEvent *aDescendantAccEvent);
/**
* Coalesce text change events caused by sibling hide events.
*/
void CoalesceTextChangeEventsFor(AccHideEvent* aTailEvent,
AccHideEvent* aThisEvent);
/**
* Create text change event caused by hide event. When a node is hidden or
* removed, the text in an ancestor hyper text will lose characters. Create
* text change event unless the node is being removed or frame is being
* destroyed.
*/
void CreateTextChangeEventFor(AccHideEvent* aEvent);
/**
* Indicates whether we're waiting on a refresh notification from our
* presshell to flush events

View File

@ -96,6 +96,8 @@ const WIN = (navigator.platform.indexOf("Win") != -1)? true : false;
////////////////////////////////////////////////////////////////////////////////
// Accessible general
const kEmbedChar = String.fromCharCode(0xfffc);
/**
* nsIAccessibleRetrieval, initialized when test is loaded.
*/

View File

@ -76,8 +76,7 @@
*/
function removeChildDiv(aID, aChildId)
{
this.__proto__ = new textRemoveInvoker(aID, 5, 6,
String.fromCharCode(0xfffc));
this.__proto__ = new textRemoveInvoker(aID, 5, 6, kEmbedChar);
this.invoke = function removeChildDiv_invoke()
{
@ -97,6 +96,34 @@
}
}
/**
* Remove children from text container from first to last child or vice
* versa.
*/
function removeChildren(aID, aLastToFirst, aStart, aEnd, aText)
{
this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText);
this.invoke = function removeChildren_invoke()
{
ensureAccessibleTree(this.DOMNode);
if (aLastToFirst) {
while (this.DOMNode.firstChild)
this.DOMNode.removeChild(this.DOMNode.lastChild);
} else {
while (this.DOMNode.firstChild)
this.DOMNode.removeChild(this.DOMNode.firstChild);
}
}
this.getID = function removeChildren_getID()
{
return "remove children of " + prettyName(aID) +
(aLastToFirst ? " from last to first" : " from first to last");
}
}
/**
* Remove text from HTML input.
*/
@ -156,7 +183,7 @@
////////////////////////////////////////////////////////////////////////////
// Do tests
var gQueue = null;
// gA11yEventDumpID = "eventdump"; // debug stuff
//gA11yEventDumpID = "eventdump"; // debug stuff
function doTests()
{
@ -169,6 +196,11 @@
// Remove embedded character.
gQueue.push(new removeChildDiv("div", 1));
// Remove all children.
var text = kEmbedChar + "txt" + kEmbedChar;
gQueue.push(new removeChildren("div2", true, 0, 5, text));
gQueue.push(new removeChildren("div3", false, 0, 5, text));
// Text remove from text node within hypertext accessible.
gQueue.push(new removeTextFromInput("input", 1, 3, "al"));
@ -193,11 +225,16 @@
href="https://bugzilla.mozilla.org/show_bug.cgi?id=566293"
title=" wrong length of text remove event when inaccessible node containing accessible nodes is removed">
Mozilla Bug 566293
</a>
</a><br>
<a target="_blank"
href="https://bugzilla.mozilla.org/show_bug.cgi?id=570710"
title="Avoid extra array traversal during text event creation">
Mozilla Bug 570710
</a><br>
<a target="_blank"
href="https://bugzilla.mozilla.org/show_bug.cgi?id=574003"
title="Coalesce text events on nodes removal">
Mozilla Bug 574003
</a>
<p id="display"></p>
@ -208,6 +245,8 @@
<p id="p"><span><span>333</span><span>22</span></span>1111</p>
<div id="div">hello<div>hello</div>hello</div>
<div id="div2"><div>txt</div>txt<div>txt</div></div>
<div id="div3"><div>txt</div>txt<div>txt</div></div>
<input id="input" value="value">
<div contentEditable="true" id="editable">value</div>
<div contentEditable="true" id="editable2"><span>value</span></div>