mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-10 17:24:29 +00:00
Bug 626660, part2 - fix text diff algorithm, r=davidb, sr=neil, a=betaN
This commit is contained in:
parent
0678b1c95d
commit
e4cf999060
@ -77,6 +77,7 @@ CPPSRCS = \
|
||||
nsTextAccessible.cpp \
|
||||
nsTextEquivUtils.cpp \
|
||||
nsTextAttrs.cpp \
|
||||
TextUpdater.cpp \
|
||||
$(NULL)
|
||||
|
||||
EXPORTS = \
|
||||
|
@ -44,6 +44,7 @@
|
||||
#include "nsDocAccessible.h"
|
||||
#include "nsEventShell.h"
|
||||
#include "nsTextAccessible.h"
|
||||
#include "TextUpdater.h"
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@ -574,287 +575,6 @@ NotificationController::CreateTextChangeEventFor(AccMutationEvent* aEvent)
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Notification controller: text leaf accessible text update
|
||||
|
||||
/**
|
||||
* Used to find a difference between old and new text and fire text change
|
||||
* events.
|
||||
*/
|
||||
class TextUpdater
|
||||
{
|
||||
public:
|
||||
TextUpdater(nsDocAccessible* aDocument, nsTextAccessible* aTextLeaf) :
|
||||
mDocument(aDocument), mTextLeaf(aTextLeaf) { }
|
||||
~TextUpdater() { mDocument = nsnull; mTextLeaf = nsnull; }
|
||||
|
||||
/**
|
||||
* Update text of the text leaf accessible, fire text change events for its
|
||||
* container hypertext accessible.
|
||||
*/
|
||||
void Run(const nsAString& aNewText);
|
||||
|
||||
private:
|
||||
TextUpdater();
|
||||
TextUpdater(const TextUpdater&);
|
||||
TextUpdater& operator = (const TextUpdater&);
|
||||
|
||||
/**
|
||||
* Fire text change events based on difference between strings.
|
||||
*/
|
||||
void FindDiffNFireEvents(const nsDependentSubstring& aStr1,
|
||||
const nsDependentSubstring& aStr2,
|
||||
PRUint32** aMatrix,
|
||||
PRUint32 aStartOffset);
|
||||
|
||||
/**
|
||||
* Change type used to describe the diff between strings.
|
||||
*/
|
||||
enum ChangeType {
|
||||
eNoChange,
|
||||
eInsertion,
|
||||
eRemoval,
|
||||
eSubstitution
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to fire text change events.
|
||||
*/
|
||||
inline void MayFireEvent(nsAString* aInsertedText, nsAString* aRemovedText,
|
||||
PRUint32 aOffset, ChangeType* aChange)
|
||||
{
|
||||
if (*aChange == eNoChange)
|
||||
return;
|
||||
|
||||
if (*aChange == eRemoval || *aChange == eSubstitution) {
|
||||
FireEvent(*aRemovedText, aOffset, PR_FALSE);
|
||||
aRemovedText->Truncate();
|
||||
}
|
||||
|
||||
if (*aChange == eInsertion || *aChange == eSubstitution) {
|
||||
FireEvent(*aInsertedText, aOffset, PR_TRUE);
|
||||
aInsertedText->Truncate();
|
||||
}
|
||||
|
||||
*aChange = eNoChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire text change event.
|
||||
*/
|
||||
void FireEvent(const nsAString& aModText, PRUint32 aOffset, PRBool aType);
|
||||
|
||||
private:
|
||||
nsDocAccessible* mDocument;
|
||||
nsTextAccessible* mTextLeaf;
|
||||
};
|
||||
|
||||
void
|
||||
TextUpdater::Run(const nsAString& aNewText)
|
||||
{
|
||||
NS_ASSERTION(mTextLeaf, "No text leaf accessible?");
|
||||
|
||||
const nsString& oldText = mTextLeaf->Text();
|
||||
PRUint32 oldLen = oldText.Length(), newLen = aNewText.Length();
|
||||
PRUint32 minLen = oldLen < newLen ? oldLen : newLen;
|
||||
|
||||
// Skip coinciding begin substrings.
|
||||
PRUint32 skipIdx = 0;
|
||||
for (; skipIdx < minLen; skipIdx++) {
|
||||
if (aNewText[skipIdx] != oldText[skipIdx])
|
||||
break;
|
||||
}
|
||||
|
||||
// No change, text append or removal to/from the end.
|
||||
if (skipIdx == minLen) {
|
||||
if (oldLen == newLen)
|
||||
return;
|
||||
|
||||
// If text has been appended to the end, fire text inserted event.
|
||||
if (oldLen < newLen) {
|
||||
FireEvent(Substring(aNewText, oldLen), oldLen, PR_TRUE);
|
||||
mTextLeaf->SetText(aNewText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Text has been removed from the end, fire text removed event.
|
||||
FireEvent(Substring(oldText, newLen), newLen, PR_FALSE);
|
||||
mTextLeaf->SetText(aNewText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trim coinciding substrings from the end.
|
||||
PRUint32 endIdx = minLen;
|
||||
if (oldLen < newLen) {
|
||||
PRUint32 delta = newLen - oldLen;
|
||||
for (; endIdx > skipIdx; endIdx--) {
|
||||
if (aNewText[endIdx + delta] != oldText[endIdx])
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
PRUint32 delta = oldLen - newLen;
|
||||
for (; endIdx > skipIdx; endIdx--) {
|
||||
if (aNewText[endIdx] != oldText[endIdx + delta])
|
||||
break;
|
||||
}
|
||||
}
|
||||
PRUint32 oldEndIdx = oldLen - minLen + endIdx;
|
||||
PRUint32 newEndIdx = newLen - minLen + endIdx;
|
||||
|
||||
// Find the difference starting from start character, we can skip initial and
|
||||
// final coinciding characters since they don't affect on the Levenshtein
|
||||
// distance.
|
||||
|
||||
const nsDependentSubstring& str1 =
|
||||
Substring(oldText, skipIdx, oldEndIdx - skipIdx);
|
||||
const nsDependentSubstring& str2 =
|
||||
Substring(aNewText, skipIdx, newEndIdx - skipIdx);
|
||||
|
||||
// Compute the matrix.
|
||||
PRUint32 len1 = str1.Length() + 1, len2 = str2.Length() + 1;
|
||||
|
||||
PRUint32** matrix = new PRUint32*[len1];
|
||||
for (PRUint32 i = 0; i < len1; i++)
|
||||
matrix[i] = new PRUint32[len2];
|
||||
|
||||
matrix[0][0] = 0;
|
||||
|
||||
for (PRUint32 i = 1; i < len1; i++)
|
||||
matrix[i][0] = i;
|
||||
|
||||
for (PRUint32 j = 1; j < len2; j++)
|
||||
matrix[0][j] = j;
|
||||
|
||||
for (PRUint32 i = 1; i < len1; i++) {
|
||||
for (PRUint32 j = 1; j < len2; j++) {
|
||||
if (str1[i - 1] != str2[j - 1]) {
|
||||
PRUint32 left = matrix[i - 1][j];
|
||||
PRUint32 up = matrix[i][j - 1];
|
||||
|
||||
PRUint32 upleft = matrix[i - 1][j - 1];
|
||||
matrix[i][j] =
|
||||
(left < up ? (upleft < left ? upleft : left) :
|
||||
(upleft < up ? upleft : up)) + 1;
|
||||
} else {
|
||||
matrix[i][j] = matrix[i - 1][j - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FindDiffNFireEvents(str1, str2, matrix, skipIdx);
|
||||
|
||||
for (PRUint32 i = 0; i < len1; i++)
|
||||
delete[] matrix[i];
|
||||
delete[] matrix;
|
||||
|
||||
mTextLeaf->SetText(aNewText);
|
||||
}
|
||||
|
||||
void
|
||||
TextUpdater::FindDiffNFireEvents(const nsDependentSubstring& aStr1,
|
||||
const nsDependentSubstring& aStr2,
|
||||
PRUint32** aMatrix,
|
||||
PRUint32 aStartOffset)
|
||||
{
|
||||
// Find the difference.
|
||||
ChangeType change = eNoChange;
|
||||
nsAutoString insertedText;
|
||||
nsAutoString removedText;
|
||||
PRUint32 offset = 0;
|
||||
|
||||
PRInt32 i = aStr1.Length(), j = aStr2.Length();
|
||||
while (i >= 0 && j >= 0) {
|
||||
if (aMatrix[i][j] == 0) {
|
||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
||||
return;
|
||||
}
|
||||
|
||||
// move up left
|
||||
if (i >= 1 && j >= 1) {
|
||||
// no change
|
||||
if (aStr1[i - 1] == aStr2[j - 1]) {
|
||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
||||
|
||||
i--; j--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// substitution
|
||||
if (aMatrix[i][j] == aMatrix[i - 1][j - 1] + 1) {
|
||||
if (change != eSubstitution)
|
||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
||||
|
||||
offset = j - 1;
|
||||
insertedText.Append(aStr1[i - 1]);
|
||||
removedText.Append(aStr2[offset]);
|
||||
change = eSubstitution;
|
||||
|
||||
i--; j--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// move up, insertion
|
||||
if (j >= 1 && aMatrix[i][j] == aMatrix[i][j - 1] + 1) {
|
||||
if (change != eInsertion)
|
||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
||||
|
||||
offset = j - 1;
|
||||
insertedText.Insert(aStr2[offset], 0);
|
||||
change = eInsertion;
|
||||
|
||||
j--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// move left, removal
|
||||
if (i >= 1 && aMatrix[i][j] == aMatrix[i - 1][j] + 1) {
|
||||
if (change != eRemoval) {
|
||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
||||
|
||||
offset = j;
|
||||
}
|
||||
|
||||
removedText.Insert(aStr1[i - 1], 0);
|
||||
change = eRemoval;
|
||||
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
NS_NOTREACHED("Huh?");
|
||||
return;
|
||||
}
|
||||
|
||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
||||
}
|
||||
|
||||
void
|
||||
TextUpdater::FireEvent(const nsAString& aModText, PRUint32 aOffset,
|
||||
PRBool aIsInserted)
|
||||
{
|
||||
nsAccessible* parent = mTextLeaf->GetParent();
|
||||
NS_ASSERTION(parent, "No parent for text leaf!");
|
||||
|
||||
nsHyperTextAccessible* hyperText = parent->AsHyperText();
|
||||
NS_ASSERTION(hyperText, "Text leaf parnet is not hyper text!");
|
||||
|
||||
PRInt32 textLeafOffset = hyperText->GetChildOffset(mTextLeaf, PR_TRUE);
|
||||
NS_ASSERTION(textLeafOffset != -1,
|
||||
"Text leaf hasn't offset within hyper text!");
|
||||
|
||||
// Fire text change event.
|
||||
nsRefPtr<AccEvent> textChangeEvent =
|
||||
new AccTextChangeEvent(hyperText, textLeafOffset + aOffset, aModText,
|
||||
aIsInserted);
|
||||
mDocument->FireDelayedAccessibleEvent(textChangeEvent);
|
||||
|
||||
// Fire value change event.
|
||||
if (hyperText->Role() == nsIAccessibleRole::ROLE_ENTRY) {
|
||||
nsRefPtr<AccEvent> valueChangeEvent =
|
||||
new AccEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, hyperText,
|
||||
eAutoDetect, AccEvent::eRemoveDupes);
|
||||
mDocument->FireDelayedAccessibleEvent(valueChangeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
PLDHashOperator
|
||||
NotificationController::TextEnumerator(nsCOMPtrHashKey<nsIContent>* aEntry,
|
||||
void* aUserArg)
|
||||
@ -927,9 +647,7 @@ NotificationController::TextEnumerator(nsCOMPtrHashKey<nsIContent>* aEntry,
|
||||
NS_ConvertUTF16toUTF8(text).get());
|
||||
#endif
|
||||
|
||||
TextUpdater updater(document, textAcc->AsTextLeaf());
|
||||
updater.Run(text);
|
||||
|
||||
TextUpdater::Run(document, textAcc->AsTextLeaf(), text);
|
||||
return PL_DHASH_NEXT;
|
||||
}
|
||||
|
||||
|
263
accessible/src/base/TextUpdater.cpp
Normal file
263
accessible/src/base/TextUpdater.cpp
Normal file
@ -0,0 +1,263 @@
|
||||
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* ***** BEGIN LICENSE BLOCK *****
|
||||
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is mozilla.org code.
|
||||
*
|
||||
* The Initial Developer of the Original Code is
|
||||
* Mozilla Foundation.
|
||||
* Portions created by the Initial Developer are Copyright (C) 2011
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s):
|
||||
* Alexander Surkov <surkov.alexander@gmail.com> (original author)
|
||||
*
|
||||
* Alternatively, the contents of this file may be used under the terms of
|
||||
* either of the GNU General Public License Version 2 or later (the "GPL"),
|
||||
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||
* of those above. If you wish to allow use of your version of this file only
|
||||
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||
* use your version of this file under the terms of the MPL, indicate your
|
||||
* decision by deleting the provisions above and replace them with the notice
|
||||
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||
* the provisions above, a recipient may use your version of this file under
|
||||
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
#include "TextUpdater.h"
|
||||
|
||||
#include "nsDocAccessible.h"
|
||||
#include "nsTextAccessible.h"
|
||||
|
||||
void
|
||||
TextUpdater::Run(nsDocAccessible* aDocument, nsTextAccessible* aTextLeaf,
|
||||
const nsAString& aNewText)
|
||||
{
|
||||
NS_ASSERTION(aTextLeaf, "No text leaf accessible?");
|
||||
|
||||
const nsString& oldText = aTextLeaf->Text();
|
||||
PRUint32 oldLen = oldText.Length(), newLen = aNewText.Length();
|
||||
PRUint32 minLen = NS_MIN(oldLen, newLen);
|
||||
|
||||
// Skip coinciding begin substrings.
|
||||
PRUint32 skipStart = 0;
|
||||
for (; skipStart < minLen; skipStart++) {
|
||||
if (aNewText[skipStart] != oldText[skipStart])
|
||||
break;
|
||||
}
|
||||
|
||||
// The text was changed. Do update.
|
||||
if (skipStart != minLen || oldLen != newLen) {
|
||||
TextUpdater updater(aDocument, aTextLeaf);
|
||||
updater.DoUpdate(aNewText, oldText, skipStart);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
TextUpdater::DoUpdate(const nsAString& aNewText, const nsAString& aOldText,
|
||||
PRUint32 aSkipStart)
|
||||
{
|
||||
nsAccessible* parent = mTextLeaf->GetParent();
|
||||
NS_ASSERTION(parent, "No parent for text leaf!");
|
||||
|
||||
mHyperText = parent->AsHyperText();
|
||||
if (!mHyperText) {
|
||||
NS_ERROR("Text leaf parent is not hypertext!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the text leaf accessible offset and invalidate cached offsets after it.
|
||||
mTextOffset = mHyperText->GetChildOffset(mTextLeaf, PR_TRUE);
|
||||
NS_ASSERTION(mTextOffset != -1,
|
||||
"Text leaf hasn't offset within hyper text!");
|
||||
|
||||
PRUint32 oldLen = aOldText.Length(), newLen = aNewText.Length();
|
||||
PRUint32 minLen = NS_MIN(oldLen, newLen);
|
||||
|
||||
// Text was appended or removed to/from the end.
|
||||
if (aSkipStart == minLen) {
|
||||
// If text has been appended to the end, fire text inserted event.
|
||||
if (oldLen < newLen) {
|
||||
UpdateTextNFireEvent(aNewText, Substring(aNewText, oldLen),
|
||||
oldLen, PR_TRUE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Text has been removed from the end, fire text removed event.
|
||||
UpdateTextNFireEvent(aNewText, Substring(aOldText, newLen),
|
||||
newLen, PR_FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trim coinciding substrings from the end.
|
||||
PRUint32 skipEnd = 0;
|
||||
while (minLen - skipEnd > aSkipStart &&
|
||||
aNewText[newLen - skipEnd - 1] == aOldText[oldLen - skipEnd - 1]) {
|
||||
skipEnd++;
|
||||
}
|
||||
|
||||
// Text was appended or removed to/from the start.
|
||||
if (skipEnd == minLen) {
|
||||
// If text has been appended to the start, fire text inserted event.
|
||||
if (oldLen < newLen) {
|
||||
UpdateTextNFireEvent(aNewText, Substring(aNewText, 0, newLen - skipEnd),
|
||||
0, PR_TRUE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Text has been removed from the start, fire text removed event.
|
||||
UpdateTextNFireEvent(aNewText, Substring(aOldText, 0, oldLen - skipEnd),
|
||||
0, PR_FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the difference between strings and fire events.
|
||||
// Note: we can skip initial and final coinciding characters since they don't
|
||||
// affect the Levenshtein distance.
|
||||
|
||||
PRInt32 strLen1 = oldLen - aSkipStart - skipEnd;
|
||||
PRInt32 strLen2 = newLen - aSkipStart - skipEnd;
|
||||
|
||||
const nsAString& str1 = Substring(aOldText, aSkipStart, strLen1);
|
||||
const nsAString& str2 = Substring(aNewText, aSkipStart, strLen2);
|
||||
|
||||
// Increase offset of the text leaf on skipped characters amount.
|
||||
mTextOffset += aSkipStart;
|
||||
|
||||
// Compute the flat structured matrix need to compute the difference.
|
||||
PRUint32 len1 = strLen1 + 1, len2 = strLen2 + 1;
|
||||
PRUint32* entries = new PRUint32[len1 * len2];
|
||||
|
||||
for (PRUint32 colIdx = 0; colIdx < len1; colIdx++)
|
||||
entries[colIdx] = colIdx;
|
||||
|
||||
PRUint32* row = entries;
|
||||
for (PRUint32 rowIdx = 1; rowIdx < len2; rowIdx++) {
|
||||
PRUint32* prevRow = row;
|
||||
row += len1;
|
||||
row[0] = rowIdx;
|
||||
for (PRUint32 colIdx = 1; colIdx < len1; colIdx++) {
|
||||
if (str1[colIdx - 1] != str2[rowIdx - 1]) {
|
||||
PRUint32 left = row[colIdx - 1];
|
||||
PRUint32 up = prevRow[colIdx];
|
||||
PRUint32 upleft = prevRow[colIdx - 1];
|
||||
row[colIdx] = NS_MIN(upleft, NS_MIN(left, up)) + 1;
|
||||
} else {
|
||||
row[colIdx] = prevRow[colIdx - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute events based on the difference.
|
||||
nsTArray<nsRefPtr<AccEvent> > events;
|
||||
ComputeTextChangeEvents(str1, str2, entries, events);
|
||||
|
||||
delete [] entries;
|
||||
|
||||
// Fire events.
|
||||
for (PRInt32 idx = events.Length() - 1; idx >= 0; idx--)
|
||||
mDocument->FireDelayedAccessibleEvent(events[idx]);
|
||||
|
||||
if (mHyperText->Role() == nsIAccessibleRole::ROLE_ENTRY) {
|
||||
nsRefPtr<AccEvent> valueChangeEvent =
|
||||
new AccEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, mHyperText,
|
||||
eAutoDetect, AccEvent::eRemoveDupes);
|
||||
mDocument->FireDelayedAccessibleEvent(valueChangeEvent);
|
||||
}
|
||||
|
||||
// Update the text.
|
||||
mTextLeaf->SetText(aNewText);
|
||||
}
|
||||
|
||||
void
|
||||
TextUpdater::ComputeTextChangeEvents(const nsAString& aStr1,
|
||||
const nsAString& aStr2,
|
||||
PRUint32* aEntries,
|
||||
nsTArray<nsRefPtr<AccEvent> >& aEvents)
|
||||
{
|
||||
PRInt32 colIdx = aStr1.Length(), rowIdx = aStr2.Length();
|
||||
|
||||
// Point at which strings last matched.
|
||||
PRInt32 colEnd = colIdx;
|
||||
PRInt32 rowEnd = rowIdx;
|
||||
|
||||
PRInt32 colLen = colEnd + 1;
|
||||
PRUint32* row = aEntries + rowIdx * colLen;
|
||||
PRInt32 dist = row[colIdx]; // current Levenshtein distance
|
||||
while (rowIdx && colIdx) { // stop when we can't move diagonally
|
||||
if (aStr1[colIdx - 1] == aStr2[rowIdx - 1]) { // match
|
||||
if (rowIdx < rowEnd) { // deal with any pending insertion
|
||||
FireInsertEvent(Substring(aStr2, rowIdx, rowEnd - rowIdx),
|
||||
rowIdx, aEvents);
|
||||
}
|
||||
if (colIdx < colEnd) { // deal with any pending deletion
|
||||
FireDeleteEvent(Substring(aStr1, colIdx, colEnd - colIdx),
|
||||
rowIdx, aEvents);
|
||||
}
|
||||
|
||||
colEnd = --colIdx; // reset the match point
|
||||
rowEnd = --rowIdx;
|
||||
row -= colLen;
|
||||
continue;
|
||||
}
|
||||
--dist;
|
||||
if (dist == row[colIdx - 1 - colLen]) { // substitution
|
||||
--colIdx;
|
||||
--rowIdx;
|
||||
row -= colLen;
|
||||
continue;
|
||||
}
|
||||
if (dist == row[colIdx - colLen]) { // insertion
|
||||
--rowIdx;
|
||||
row -= colLen;
|
||||
continue;
|
||||
}
|
||||
if (dist == row[colIdx - 1]) { // deletion
|
||||
--colIdx;
|
||||
continue;
|
||||
}
|
||||
NS_NOTREACHED("huh?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (rowEnd)
|
||||
FireInsertEvent(Substring(aStr2, 0, rowEnd), 0, aEvents);
|
||||
if (colEnd)
|
||||
FireDeleteEvent(Substring(aStr1, 0, colEnd), 0, aEvents);
|
||||
}
|
||||
|
||||
void
|
||||
TextUpdater::UpdateTextNFireEvent(const nsAString& aNewText,
|
||||
const nsAString& aChangeText,
|
||||
PRUint32 aAddlOffset,
|
||||
PRBool aIsInserted)
|
||||
{
|
||||
// Fire text change event.
|
||||
nsRefPtr<AccEvent> textChangeEvent =
|
||||
new AccTextChangeEvent(mHyperText, mTextOffset + aAddlOffset, aChangeText,
|
||||
aIsInserted);
|
||||
mDocument->FireDelayedAccessibleEvent(textChangeEvent);
|
||||
|
||||
// Fire value change event.
|
||||
if (mHyperText->Role() == nsIAccessibleRole::ROLE_ENTRY) {
|
||||
nsRefPtr<AccEvent> valueChangeEvent =
|
||||
new AccEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, mHyperText,
|
||||
eAutoDetect, AccEvent::eRemoveDupes);
|
||||
mDocument->FireDelayedAccessibleEvent(valueChangeEvent);
|
||||
}
|
||||
|
||||
// Update the text.
|
||||
mTextLeaf->SetText(aNewText);
|
||||
}
|
124
accessible/src/base/TextUpdater.h
Normal file
124
accessible/src/base/TextUpdater.h
Normal file
@ -0,0 +1,124 @@
|
||||
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* ***** BEGIN LICENSE BLOCK *****
|
||||
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is mozilla.org code.
|
||||
*
|
||||
* The Initial Developer of the Original Code is
|
||||
* Mozilla Foundation.
|
||||
* Portions created by the Initial Developer are Copyright (C) 2011
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s):
|
||||
* Alexander Surkov <surkov.alexander@gmail.com> (original author)
|
||||
*
|
||||
* Alternatively, the contents of this file may be used under the terms of
|
||||
* either of the GNU General Public License Version 2 or later (the "GPL"),
|
||||
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||
* of those above. If you wish to allow use of your version of this file only
|
||||
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||
* use your version of this file under the terms of the MPL, indicate your
|
||||
* decision by deleting the provisions above and replace them with the notice
|
||||
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||
* the provisions above, a recipient may use your version of this file under
|
||||
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
#ifndef TextUpdater_h_
|
||||
#define TextUpdater_h_
|
||||
|
||||
#include "AccEvent.h"
|
||||
#include "nsHyperTextAccessible.h"
|
||||
|
||||
/**
|
||||
* Used to find a difference between old and new text and fire text change
|
||||
* events.
|
||||
*/
|
||||
class TextUpdater
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Start text of the text leaf update.
|
||||
*/
|
||||
static void Run(nsDocAccessible* aDocument, nsTextAccessible* aTextLeaf,
|
||||
const nsAString& aNewText);
|
||||
|
||||
private:
|
||||
TextUpdater(nsDocAccessible* aDocument, nsTextAccessible* aTextLeaf) :
|
||||
mDocument(aDocument), mTextLeaf(aTextLeaf), mHyperText(nsnull),
|
||||
mTextOffset(-1) { }
|
||||
|
||||
~TextUpdater()
|
||||
{ mDocument = nsnull; mTextLeaf = nsnull; mHyperText = nsnull; }
|
||||
|
||||
/**
|
||||
* Update text of the text leaf accessible, fire text change and value change
|
||||
* (if applicable) events for its container hypertext accessible.
|
||||
*/
|
||||
void DoUpdate(const nsAString& aNewText, const nsAString& aOldText,
|
||||
PRUint32 aSkipStart);
|
||||
|
||||
private:
|
||||
TextUpdater();
|
||||
TextUpdater(const TextUpdater&);
|
||||
TextUpdater& operator=(const TextUpdater&);
|
||||
|
||||
/**
|
||||
* Fire text change events based on difference between strings.
|
||||
*/
|
||||
void ComputeTextChangeEvents(const nsAString& aStr1,
|
||||
const nsAString& aStr2,
|
||||
PRUint32* aEntries,
|
||||
nsTArray<nsRefPtr<AccEvent> >& aEvents);
|
||||
|
||||
/**
|
||||
* Helper to create text change events for inserted text.
|
||||
*/
|
||||
inline void FireInsertEvent(const nsAString& aText, PRUint32 aAddlOffset,
|
||||
nsTArray<nsRefPtr<AccEvent> >& aEvents)
|
||||
{
|
||||
nsRefPtr<AccEvent> event =
|
||||
new AccTextChangeEvent(mHyperText, mTextOffset + aAddlOffset,
|
||||
aText, PR_TRUE);
|
||||
aEvents.AppendElement(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create text change events for removed text.
|
||||
*/
|
||||
inline void FireDeleteEvent(const nsAString& aText, PRUint32 aAddlOffset,
|
||||
nsTArray<nsRefPtr<AccEvent> >& aEvents)
|
||||
{
|
||||
nsRefPtr<AccEvent> event =
|
||||
new AccTextChangeEvent(mHyperText, mTextOffset + aAddlOffset,
|
||||
aText, PR_FALSE);
|
||||
aEvents.AppendElement(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the text and fire text change/value change events.
|
||||
*/
|
||||
void UpdateTextNFireEvent(const nsAString& aNewText,
|
||||
const nsAString& aChangeText, PRUint32 aAddlOffset,
|
||||
PRBool aIsInserted);
|
||||
|
||||
private:
|
||||
nsDocAccessible* mDocument;
|
||||
nsTextAccessible* mTextLeaf;
|
||||
nsHyperTextAccessible* mHyperText;
|
||||
PRInt32 mTextOffset;
|
||||
};
|
||||
|
||||
#endif
|
@ -331,7 +331,7 @@ function eventQueue(aEventType)
|
||||
var idx = 0;
|
||||
for (; idx < this.mEventSeq.length; idx++) {
|
||||
if (!this.isEventUnexpected(idx) && (invoker.wasCaught[idx] == true) &&
|
||||
this.compareEvents(idx, aEvent)) {
|
||||
this.isAlreadyCaught(idx, aEvent)) {
|
||||
|
||||
var msg = "Doubled event { event type: " +
|
||||
this.getEventTypeAsString(idx) + ", target: " +
|
||||
@ -543,6 +543,15 @@ function eventQueue(aEventType)
|
||||
return target1 == target2;
|
||||
}
|
||||
|
||||
this.isAlreadyCaught = function eventQueue_isAlreadyCaught(aIdx, aEvent)
|
||||
{
|
||||
// We don't have stored info about handled event other than its type and
|
||||
// target, thus we should filter text change events since they may occur
|
||||
// on the same element because of complex changes.
|
||||
return this.compareEvents(aIdx, aEvent) &&
|
||||
!(aEvent instanceof nsIAccessibleTextChangeEvent);
|
||||
}
|
||||
|
||||
this.checkEvent = function eventQueue_checkEvent(aIdx, aEvent)
|
||||
{
|
||||
var eventItem = this.mEventSeq[aIdx];
|
||||
|
@ -71,6 +71,7 @@ _TEST_FILES =\
|
||||
test_scroll.xul \
|
||||
test_selection.html \
|
||||
test_statechange.html \
|
||||
test_text_alg.html \
|
||||
test_text.html \
|
||||
test_textattrchange.html \
|
||||
test_tree.xul \
|
||||
|
205
accessible/tests/mochitest/events/test_text_alg.html
Normal file
205
accessible/tests/mochitest/events/test_text_alg.html
Normal file
@ -0,0 +1,205 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Accessible text update algorithm testing</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/MochiKit/packed.js"></script>
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
|
||||
|
||||
<script type="application/javascript"
|
||||
src="../common.js"></script>
|
||||
<script type="application/javascript"
|
||||
src="../events.js"></script>
|
||||
|
||||
<script type="application/javascript">
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Invokers
|
||||
|
||||
const kRemoval = 0;
|
||||
const kInsertion = 1;
|
||||
|
||||
function changeText(aContainerID, aValue, aEventList)
|
||||
{
|
||||
this.containerNode = getNode(aContainerID);
|
||||
this.textNode = this.containerNode.firstChild;
|
||||
this.textData = this.textNode.data;
|
||||
|
||||
this.eventSeq = [ ];
|
||||
for (var i = 0; i < aEventList.length; i++) {
|
||||
var isInserted = aEventList[i][0];
|
||||
var str = aEventList[i][1];
|
||||
var offset = aEventList[i][2];
|
||||
var checker = new textChangeChecker(this.containerNode, offset,
|
||||
offset + str.length, str,
|
||||
isInserted);
|
||||
this.eventSeq.push(checker);
|
||||
}
|
||||
|
||||
this.invoke = function changeText_invoke()
|
||||
{
|
||||
this.textNode.data = aValue;
|
||||
}
|
||||
|
||||
this.getID = function changeText_getID()
|
||||
{
|
||||
return "change text '" + this.textData + "' -> " + this.textNode.data +
|
||||
"for " + prettyName(this.containerNode);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Do tests
|
||||
|
||||
//gA11yEventDumpID = "eventdump"; // debug stuff
|
||||
//gA11yEventDumpToConsole = true;
|
||||
|
||||
var gQueue = null;
|
||||
function doTests()
|
||||
{
|
||||
gQueue = new eventQueue();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// wqrema -> tqb: substitution coalesced with removal
|
||||
|
||||
var events = [
|
||||
[ kRemoval, "w", 0 ], // wqrema -> qrema
|
||||
[ kInsertion, "t", 0], // qrema -> tqrema
|
||||
[ kRemoval, "rema", 2 ], // tqrema -> tq
|
||||
[ kInsertion, "b", 2] // tq -> tqb
|
||||
];
|
||||
gQueue.push(new changeText("p1", "tqb", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// b -> insa: substitution coalesced with insertion (complex substitution)
|
||||
|
||||
events = [
|
||||
[ kRemoval, "b", 0 ], // b ->
|
||||
[ kInsertion, "insa", 0] // -> insa
|
||||
];
|
||||
gQueue.push(new changeText("p2", "insa", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// abc -> def: coalesced substitutions
|
||||
|
||||
events = [
|
||||
[ kRemoval, "abc", 0 ], // abc ->
|
||||
[ kInsertion, "def", 0] // -> def
|
||||
];
|
||||
gQueue.push(new changeText("p3", "def", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// abcabc -> abcDEFabc: coalesced insertions
|
||||
|
||||
events = [
|
||||
[ kInsertion, "DEF", 3] // abcabc -> abcDEFabc
|
||||
];
|
||||
gQueue.push(new changeText("p4", "abcDEFabc", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// abc -> defabc: insertion into begin
|
||||
|
||||
events = [
|
||||
[ kInsertion, "def", 0] // abc -> defabc
|
||||
];
|
||||
gQueue.push(new changeText("p5", "defabc", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// abc -> abcdef: insertion into end
|
||||
|
||||
events = [
|
||||
[ kInsertion, "def", 3] // abc -> abcdef
|
||||
];
|
||||
gQueue.push(new changeText("p6", "abcdef", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// defabc -> abc: removal from begin
|
||||
|
||||
events = [
|
||||
[ kRemoval, "def", 0] // defabc -> abc
|
||||
];
|
||||
gQueue.push(new changeText("p7", "abc", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// abcdef -> abc: removal from the end
|
||||
|
||||
events = [
|
||||
[ kRemoval, "def", 3] // abcdef -> abc
|
||||
];
|
||||
gQueue.push(new changeText("p8", "abc", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// abcDEFabc -> abcabc: coalesced removals
|
||||
|
||||
events = [
|
||||
[ kRemoval, "DEF", 3] // abcDEFabc -> abcabc
|
||||
];
|
||||
gQueue.push(new changeText("p9", "abcabc", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// !abcdef@ -> @axbcef!: insertion, deletion and substitutions
|
||||
|
||||
events = [
|
||||
[ kRemoval, "!", 0 ], // !abcdef@ -> abcdef@
|
||||
[ kInsertion, "@", 0], // abcdef@ -> @abcdef@
|
||||
[ kInsertion, "x", 2 ], // @abcdef@ -> @axbcdef@
|
||||
[ kRemoval, "d", 5], // @axbcdef@ -> @axbcef@
|
||||
[ kRemoval, "@", 7 ], // @axbcef@ -> @axbcef
|
||||
[ kInsertion, "!", 7 ], // @axbcef -> @axbcef!
|
||||
];
|
||||
gQueue.push(new changeText("p10", "@axbcef!", events));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// meilenstein -> levenshtein: insertion, complex and simple substitutions
|
||||
|
||||
events = [
|
||||
[ kRemoval, "m", 0 ], // meilenstein -> eilenstein
|
||||
[ kInsertion, "l", 0], // eilenstein -> leilenstein
|
||||
[ kRemoval, "il", 2 ], // leilenstein -> leenstein
|
||||
[ kInsertion, "v", 2], // leenstein -> levenstein
|
||||
[ kInsertion, "h", 6 ], // levenstein -> levenshtein
|
||||
];
|
||||
gQueue.push(new changeText("p11", "levenshtein", events));
|
||||
|
||||
gQueue.invoke(); // Will call SimpleTest.finish();
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
addA11yLoadEvent(doTests);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<a target="_blank"
|
||||
href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660"
|
||||
title="Cache rendered text on a11y side">
|
||||
Mozilla Bug 626660
|
||||
</a>
|
||||
<br>
|
||||
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none"></div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<div id="eventdump"></div>
|
||||
|
||||
<p id="p1">wqrema</p>
|
||||
<p id="p2">b</p>
|
||||
<p id="p3">abc</p>
|
||||
<p id="p4">abcabc</p>
|
||||
<p id="p5">abc</p>
|
||||
<p id="p6">abc</p>
|
||||
<p id="p7">defabc</p>
|
||||
<p id="p8">abcdef</p>
|
||||
<p id="p9">abcDEFabc</p>
|
||||
<p id="p10">!abcdef@</p>
|
||||
<p id="p11">meilenstein</p>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user