Repository: nvaccess/nvda

Issue:
Python 3: when in UIA based edit field, the deleted character is not spoken when pressing backspace
<!--
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples
-->

### Steps to reproduce:
1. go to any edit field, for example, skype chat, unigram chat, and or windows mail UIA app
2. write something
3. press backspace
### Actual behavior:
the deleted caracter is not voiced
### Expected behavior:
deleted character should be voiced
### System configuration
#### NVDA installed/portable/running from source:
portable
#### NVDA version:
latest treshold py3 staging
#### Windows version:
windows 10 1903
#### Name and version of other software in use when reproducing the issue:
not applicable
#### Other information about your system:
n/a
### Other questions
#### Does the issue still occur after restarting your PC?
yes
#### Have you tried any other versions of NVDA? If so, please report their behaviors.
no and not applicable, as it works in alpha snapshots


Discussion:
Can you please test this with a portable copy of normal threshold (not the py3 staging branch)?
Yes, please give me the time, as i am waiting for compilation.

 

I was not able to reproduce this on latest threshold before Python 3 was merged in. Interesting ehough, I thought it was caused by me changing the postCaretMovedScriptHelper, but that doesn't seem to be the case.
I am waiting for the mege. 

I will see when it merges to threshold

 
 

Nope,

I’ve testing it now with treshold, and it is not fixed there.

It is a regression

Ive been able to look into this some more, and it's pretty complicated.

The issue is in editableText.EditableText, line 101: `if newBookmark and newBookmark!=bookmark:`

First, let's try to explain something like a code example. For this, type hello into a word document and paste this into a python console:

```
x=nav.makeTextInfo("caret")
x==x
x!=x
y=x.copy()
x==y
x!=y
y.move("character", -1)
x==y
x!=y

```

Output under Python 2 is as follows:

```
>>> x=nav.makeTextInfo("caret")
>>> x==x
True
>>> x!=x
False
>>> y=x.copy()
>>> x==y
True
>>> x!=y
True
>>> y.move("character", -1)
-1
>>> x==y
False
>>> x!=y
True
```

Output under Python 3:

```
>>> x=nav.makeTextInfo("caret")
>>> x==x
True
>>> x!=x
False
>>> y=x.copy()
>>> x==y
True
>>> x!=y
False
>>> y.move("character", -1)
0
>>> x==y
True
>>> x!=y
False
```

The important case and only difference here is the first occurrence of `x!=y`, which is True on Python 2 and False on Python 3. I'm pretty sure that this insconsistency in the `__eq__` and `__neq__` operator clarify this issue.
Actually, a positive and negative comparison could never be True both as is the case on Python 2. So, this code has probably never worked as it really should have worked in the first place.

Cc @feerrenrut
Another example:

1. In word, type hello
2. Position caret at the start of the line
3. `a=nav.makeTextInfo("caret")`
4. In the word document, press right arrow
5. `b=nav.makeTextInfo("caret")`
6. Now, `a==b` is False
7. In the word document, press backspace
8. Now, a==b is True.

This Compare method on UIA Text Ranges really behaves a bit strange. I'm afraid it is not very safe to rely on it for our equality comparison.

Resolving patch:
diff --git a/source/NVDAObjects/IAccessible/ia2Web.py b/source/NVDAObjects/IAccessible/ia2Web.py
--- a/source/NVDAObjects/IAccessible/ia2Web.py
+++ b/source/NVDAObjects/IAccessible/ia2Web.py
@@ -20,6 +20,7 @@
 
 class Ia2Web(IAccessible):
 	IAccessibleTableUsesTableCellIndexAttrib=True
+	caretMovementDetectionUsesEvents = False
 
 	def _get_positionInfo(self):
 		info=super(Ia2Web,self).positionInfo
diff --git a/source/editableText.py b/source/editableText.py
--- a/source/editableText.py
+++ b/source/editableText.py
@@ -46,10 +46,14 @@ class EditableText(TextContainerObject,ScriptableObject):
 	#: When announcing new line text: should the entire line be announced, or just text after the caret?
 	announceEntireNewLine=False
 
-	_hasCaretMoved_minWordTimeoutMs=30 #: The minimum amount of time that should elapse before checking if the word under the caret has changed
+	#: The minimum amount of time that should elapse before checking if the word under the caret has changed
+	_hasCaretMoved_minWordTimeoutMs=30
+
+	#: The maximum amount of time that may elapse before we no longer rely on caret events to detect movement.
+	_useEvents_maxTimeoutMs = 10
 
 	_caretMovementTimeoutMultiplier = 1
-	
+
 	def _hasCaretMoved(self, bookmark, retryInterval=0.01, timeout=None, origWord=None):
 		"""
 		Waits for the caret to move, for a timeout to elapse, or for a new focus event or script to be queued.
@@ -90,7 +94,18 @@ def _hasCaretMoved(self, bookmark, retryInterval=0.01, timeout=None, origWord=No
 				newInfo = self.makeTextInfo(textInfos.POSITION_CARET)
 			except (RuntimeError,NotImplementedError):
 				newInfo = None
-			# Caret events are unreliable in some controls.
+			else:
+				# Caret events are unreliable in some controls.
+				# Only use them if we consider them safe to rely on for a particular control,
+				# and only if they arrive within C{_useEvents_maxTimeoutMs} mili seconds
+				# after causing the event to occur.
+				if (
+					elapsed <= self._useEvents_maxTimeoutMs and
+					self.caretMovementDetectionUsesEvents and
+					(eventHandler.isPendingEvents("caret") or eventHandler.isPendingEvents("textChange"))
+				):
+					log.debug("Caret move detected using event. Elapsed: %d ms" % elapsed)
+					return (True,newInfo)
 			# Try to detect with bookmarks.
 			newBookmark = None
 			if newInfo:
@@ -148,6 +163,20 @@ def _caretMovementScriptHelper(self, gesture, unit):
 			eventHandler.executeEvent("caretMovementFailed", self, gesture=gesture)
 		self._caretScriptPostMovedHelper(unit,gesture,newInfo)
 
+	def _get_caretMovementDetectionUsesEvents(self) -> bool:
+		"""Returns whether or not to rely on caret and textChange events when
+		finding out whether the caret position has changed after pressing a caret movement gesture.
+		Note that if L{_useEvents_maxTimeoutMs} is elapsed,
+		relying on events is no longer reliable in most situations.
+		Therefore, any event should occur before that timeout elapses.
+		"""
+		# This class is a mixin that usually comes before other relevant classes in the mro.
+		# Therefore, try to call super first, and if that fails, return the default (C{True}.
+		try:
+			return super().caretMovementDetectionUsesEvents
+		except AttributeError:
+			return True
+
 	def script_caret_newLine(self,gesture):
 		try:
 			info=self.makeTextInfo(textInfos.POSITION_CARET)


Test patch: