Repository: nvaccess/nvda

Issue:
Cancelable speech: NVDA does not report tab titles when switching with ctrl+tab or ctrl+shift+tab
### Steps to reproduce:
1. Open Firefox
2. Open two tabs on amazon.com for example
3. Open NVDA, and make sure that you have enabled "attempt to cancel speech" in advanced settings
4. Switch between tabs with ctrl+tab and ctrl+shift+tab

### Actual behavior:
NVDA just reports the focused link, but does not report the tab title

### Expected behavior:
NVDA should report the tab title first.
Note: this issue might also occur when you switch between desktops in Windows 10.

### System configuration
#### NVDA installed/portable/running from source:
Installed

#### NVDA version:
Version: alpha-20548,e1f3ba99

#### Windows version:
Windows 10 2004

#### Name and version of other software in use when reproducing the issue:
Tested only in Firefox 78 so far, but it might occur on other browsers as well.

#### Other information about your system:

### Other questions
#### Does the issue still occur after restarting your computer?
yes

#### Have you tried any other versions of NVDA? If so, please report their behaviors.
no issue with "attempt to cancel speech" turned off.

#### If addons are disabled, is your problem still occuring?
yes

#### Did you try to run the COM registry fixing tool in NVDA menu / tools?
n/a


Discussion:
Also duplicable with Google Chrome 84.
Moreover, when Control+Insert+F is used to bring up the Find dialog, NVDA doesn't announce the title of the dialog, "Find dialog", in most instances.

Resolving patch:
diff --git a/source/eventHandler.py b/source/eventHandler.py
--- a/source/eventHandler.py
+++ b/source/eventHandler.py
@@ -153,6 +153,8 @@ def _trackFocusObject(eventName, obj) -> None:
 	if eventName == "gainFocus":
 		lastQueuedFocusObject = obj
 		setattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME, True)
+		if speech.manager._shouldDoSpeechManagerLogging():
+			log.debug(f"Changing last queued focus object: {obj!r}")
 	elif not hasattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME):
 		setattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME, False)
 
@@ -171,7 +173,8 @@ def previouslyHadFocus():
 		return getattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME, False)
 
 	def isLastFocusObj():
-		return obj is lastQueuedFocusObject
+		# Obj may have been created multiple times pointing to the same underlying object.
+		return obj is lastQueuedFocusObject or obj == lastQueuedFocusObject
 
 	def isAncestorOfCurrentFocus():
 		return obj in api.getFocusAncestors()
diff --git a/source/speech/manager.py b/source/speech/manager.py
--- a/source/speech/manager.py
+++ b/source/speech/manager.py
@@ -80,7 +80,13 @@ def _speechManagerUnitTest(msg, *args, **kwargs) -> None:
 		When
 	"""
 	if not IS_UNIT_TEST_LOG_ENABLED:
-		return _speechManagerDebug(msg, *args, **kwargs)
+		# Don't reuse _speechManagerDebug, it leads to incorrect function names in the log (all
+		# SpeechManager debug logging appears to come from _speechManagerUnitTest instead of the frame
+		# one stack higher. The codepath argument for _log could also be used to resolve this, but duplication
+		# simpler.
+		if log.isEnabledFor(log.DEBUG) and _shouldDoSpeechManagerLogging():
+			log._log(log.DEBUG, f"SpeechManager- " + msg, args, **kwargs)
+		return
 	log._log(log.INFO, f"SpeechManUnitTest- " + msg, args, **kwargs)
 
 # Install the custom log handlers.
@@ -420,7 +426,7 @@ def _pushNextSpeech(self, doneSpeaking: bool):
 				if isinstance(item, IndexCommand):
 					self._indexesSpeaking.append(item.index)
 			self._cancelledLastSpeechWithSynth = False
-			log._speechManagerUnitTest(f"Assert Synth Gets: {seq}")
+			log._speechManagerUnitTest(f"Synth Gets: {seq}")
 			getSynth().speak(seq)
 
 	def _getNextPriority(self):


Test patch: