Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancellable speech #10885

Merged
merged 4 commits into from
May 8, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions source/NVDAObjects/IAccessible/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
import eventHandler
from NVDAObjects.behaviors import ProgressBar, Dialog, EditableTextWithAutoSelectDetection, FocusableUnfocusableContainer, ToolTip, Notification
from locationHelper import RectLTWH
from typing import (
Optional,
)

def getNVDAObjectFromEvent(hwnd,objectID,childID):
try:
Expand Down Expand Up @@ -680,25 +683,31 @@ def isDuplicateIAccessibleEvent(self,obj):
return False
return obj.event_windowHandle==self.event_windowHandle and obj.event_objectID==self.event_objectID and obj.event_childID==self.event_childID

def _get_shouldAllowIAccessibleFocusEvent(self):
"""Determine whether a focus event should be allowed for this object.
Normally, this checks for the focused state to help eliminate redundant or invalid focus events.
However, some implementations do not correctly set the focused state, so this must be overridden.
@return: C{True} if the focus event should be allowed.
@rtype: bool
"""
#this object or one of its ancestors must have state_focused.
def _getIndirectionsToParentWithFocus(self) -> Optional[int]:
testObj = self
indirections = 0
while testObj:
if controlTypes.STATE_FOCUSED in testObj.states:
break
parent = testObj.parent
# Cache the parent.
testObj.parent = parent
testObj = parent
indirections += 1
else:
return False
return True
return None
return indirections

def _get_shouldAllowIAccessibleFocusEvent(self):
"""Determine whether a focus event should be allowed for this object.
Normally, this checks for the focused state to help eliminate redundant or invalid focus events.
However, some implementations do not correctly set the focused state, so this must be overridden.
@return: C{True} if the focus event should be allowed.
@rtype: bool
"""
# this object or one of its ancestors must have state_focused.
indirectionsToFocus = self._getIndirectionsToParentWithFocus()
return indirectionsToFocus is not None

def _get_TextInfo(self):
if hasattr(self,'IAccessibleTextObject'):
Expand Down
12 changes: 6 additions & 6 deletions source/NVDAObjects/IAccessible/ia2Web.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#NVDAObjects/IAccessible/ia2Web.py
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2006-2017 NV Access Limited
# NVDAObjects/IAccessible/ia2Web.py
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2006-2020 NV Access Limited

"""Base classes with common support for browsers exposing IAccessible2.
"""
Expand All @@ -14,7 +14,7 @@
import controlTypes
from logHandler import log
from documentBase import DocumentWithTableNavigation
from NVDAObjects.behaviors import Dialog, WebDialog
from NVDAObjects.behaviors import Dialog, WebDialog
from . import IAccessible
from .ia2TextMozilla import MozillaCompoundTextInfo
import aria
Expand Down
3 changes: 3 additions & 0 deletions source/NVDAObjects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,9 @@ def _get_isInForeground(self):
"""
raise NotImplementedError

# Type info for auto property:
states: set

def _get_states(self):
"""Retrieves the current states of this object (example: selected, focused).
@return: a set of STATE_* constants from L{controlTypes}.
Expand Down
5 changes: 5 additions & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@
timeSinceInput = boolean(default=false)
vision = boolean(default=false)
speech = boolean(default=false)
speechManager = boolean(default=false)

[uwpOcr]
language = string(default="")
Expand All @@ -250,6 +251,10 @@

[development]
enableScratchpadDir = boolean(default=false)

[featureFlag]
feerrenrut marked this conversation as resolved.
Show resolved Hide resolved
# 0:default, 1:yes, 2:no
cancelExpiredFocusSpeech = integer(0, 2, default=0)
feerrenrut marked this conversation as resolved.
Show resolved Hide resolved
"""

#: The configuration specification
Expand Down
70 changes: 64 additions & 6 deletions source/eventHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
#Copyright (C) 2007-2017 NV Access Limited, Babbage B.V.

import threading
from typing import Optional

import queueHandler
import api
import speech
from speech.commands import _CancellableSpeechCommand
import appModuleHandler
import treeInterceptorHandler
import globalVars
Expand All @@ -33,15 +36,14 @@ def queueEvent(eventName,obj,**kwargs):
@param eventName: the name of the event type (e.g. 'gainFocus', 'nameChange')
@type eventName: string
"""
global lastQueuedFocusObject
if eventName=="gainFocus":
lastQueuedFocusObject=obj
_trackFocusObject(eventName, obj)
with _pendingEventCountsLock:
_pendingEventCountsByName[eventName]=_pendingEventCountsByName.get(eventName,0)+1
_pendingEventCountsByObj[obj]=_pendingEventCountsByObj.get(obj,0)+1
_pendingEventCountsByNameAndObj[(eventName,obj)]=_pendingEventCountsByNameAndObj.get((eventName,obj),0)+1
queueHandler.queueFunction(queueHandler.eventQueue,_queueEventCallback,eventName,obj,kwargs)


def _queueEventCallback(eventName,obj,kwargs):
with _pendingEventCountsLock:
curCount=_pendingEventCountsByName.get(eventName,0)
Expand Down Expand Up @@ -134,7 +136,54 @@ def gen(self, eventName, obj):
if func:
yield func, ()

def executeEvent(eventName,obj,**kwargs):

WAS_GAIN_FOCUS_OBJ_ATTR_NAME = "wasGainFocusObj"


def _trackFocusObject(eventName, obj) -> None:
""" Keeps track of lastQueuedFocusObject and sets wasGainFocusObj attr on objects.
:param eventName: the event type, eg "gainFocus"
:param obj: the object to track if focused
"""
global lastQueuedFocusObject
if eventName == "gainFocus":
lastQueuedFocusObject = obj
setattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME, obj is lastQueuedFocusObject)


def _getFocusLossCancellableSpeechCommand(
obj,
reason: controlTypes.OutputReason
) -> Optional[_CancellableSpeechCommand]:
if reason != controlTypes.REASON_FOCUS or not speech.manager._shouldCancelExpiredFocusEvents():
return None
from NVDAObjects import NVDAObject
if not isinstance(obj, NVDAObject):
log.warning("Unhandled object type. Expected all objects to be descendant from NVDAObject")
return None
previouslyHadFocus: bool = getattr(
obj,
WAS_GAIN_FOCUS_OBJ_ATTR_NAME,
False
)

def isSpeechStillValid():
from eventHandler import lastQueuedFocusObject
isLastFocusObj: bool = obj is lastQueuedFocusObject
stillValid = isLastFocusObj or not previouslyHadFocus

log._speechManagerDebug(
"checked if valid (isLast: %s, previouslyHad: %s): %s",
isLastFocusObj,
previouslyHadFocus,
obj.name
)
return stillValid

return _CancellableSpeechCommand(isSpeechStillValid)


def executeEvent(eventName, obj, **kwargs):
"""Executes an NVDA event.
@param eventName: the name of the event type (e.g. 'gainFocus', 'nameChange')
@type eventName: string
Expand All @@ -143,11 +192,20 @@ def executeEvent(eventName,obj,**kwargs):
@param kwargs: Additional event parameters as keyword arguments.
"""
try:
isGainFocus = eventName == "gainFocus"
# Allow NVDAObjects to redirect focus events to another object of their choosing.
if eventName=="gainFocus" and obj.focusRedirect:
if isGainFocus and obj.focusRedirect:
obj=obj.focusRedirect
sleepMode=obj.sleepMode
if eventName=="gainFocus" and not doPreGainFocus(obj,sleepMode=sleepMode):
if isGainFocus and speech.manager._shouldCancelExpiredFocusEvents():
log._speechManagerDebug("executeEvent: Removing cancelled speech commands.")
# ask speechManager to check if any of it's queued utterances should be cancelled
speech._manager.removeCancelledSpeechCommands()
# Don't skip objects without focus here. Even if an object no longer has focus, it needs to be processed
# to capture changes in document depth. For instance jumping into a list?
# This needs further investigation; when the next object gets focus, it should
# allow us to capture this information?
if isGainFocus and not doPreGainFocus(obj, sleepMode=sleepMode):
return
elif not sleepMode and eventName=="documentLoadComplete" and not doPreDocumentLoadComplete(obj):
return
Expand Down
40 changes: 40 additions & 0 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2237,6 +2237,7 @@ def onSave(self):
lang = self.languageCodes[self.languageChoice.Selection]
config.conf["uwpOcr"]["language"] = lang


class AdvancedPanelControls(wx.Panel):
"""Holds the actual controls for the Advanced Settings panel, this allows the state of the controls to
be more easily managed.
Expand Down Expand Up @@ -2324,6 +2325,41 @@ def __init__(self, parent):
self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"])
self.keyboardSupportInLegacyCheckBox.Enable(winVersion.isWin10(1607))

# Translators: This is the label for a group of advanced options in the
# Advanced settings panel
label = _("Speech")
speechGroup = guiHelper.BoxSizerHelper(
parent=self,
sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL)
)
sHelper.addItem(speechGroup)

expiredFocusSpeechChoices = [
# Translators: Label for the 'Cancel speech for expired &focus events' combobox
# in the Advanced settings panel.
_("Default (No)"),
# Translators: Label for the 'Cancel speech for expired &focus events' combobox
# in the Advanced settings panel.
_("Yes"),
# Translators: Label for the 'Cancel speech for expired &focus events' combobox
# in the Advanced settings panel.
_("No"),
]

# Translators: This is the label for combobox in the Advanced settings panel.
cancelExpiredFocusSpeechText = _("Attempt to cancel speech for expired focus events:")
self.cancelExpiredFocusSpeechCombo: wx.Choice = speechGroup.addLabeledControl(
cancelExpiredFocusSpeechText,
wx.Choice,
choices=expiredFocusSpeechChoices
)
self.cancelExpiredFocusSpeechCombo.SetSelection(
config.conf["featureFlag"]["cancelExpiredFocusSpeech"]
)
self.cancelExpiredFocusSpeechCombo.defaultValue = self._getDefaultValue(
["featureFlag", "cancelExpiredFocusSpeech"]
)

# Translators: This is the label for a group of advanced options in the
# Advanced settings panel
label = _("Browse mode")
Expand Down Expand Up @@ -2379,6 +2415,7 @@ def __init__(self, parent):
"timeSinceInput",
"vision",
"speech",
"speechManager",
]
# Translators: This is the label for a list in the
# Advanced settings panel
Expand Down Expand Up @@ -2413,6 +2450,7 @@ def haveConfigDefaultsBeenRestored(self):
and self.UIAInMSWordCheckBox.IsChecked() == self.UIAInMSWordCheckBox.defaultValue
and self.ConsoleUIACheckBox.IsChecked() == (self.ConsoleUIACheckBox.defaultValue == 'UIA')
and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue
and self.cancelExpiredFocusSpeechCombo.GetSelection() == self.cancelExpiredFocusSpeechCombo.defaultValue
and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue
and (
self.autoFocusFocusableElementsCheckBox.IsChecked()
Expand All @@ -2428,6 +2466,7 @@ def restoreToDefaults(self):
self.UIAInMSWordCheckBox.SetValue(self.UIAInMSWordCheckBox.defaultValue)
self.ConsoleUIACheckBox.SetValue(self.ConsoleUIACheckBox.defaultValue == 'UIA')
self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue)
self.cancelExpiredFocusSpeechCombo.SetValue(self.cancelExpiredFocusSpeechCombo.defaultValue)
self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue)
self.autoFocusFocusableElementsCheckBox.SetValue(self.autoFocusFocusableElementsCheckBox.defaultValue)
self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue)
Expand All @@ -2443,6 +2482,7 @@ def onSave(self):
else:
config.conf['UIA']['winConsoleImplementation'] = "auto"
config.conf["terminals"]["speakPasswords"] = self.winConsoleSpeakPasswordsCheckBox.IsChecked()
config.conf["featureFlag"]["cancelExpiredFocusSpeech"] = self.cancelExpiredFocusSpeechCombo.GetSelection()
config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked()
config.conf["virtualBuffers"]["autoFocusFocusableElements"] = self.autoFocusFocusableElementsCheckBox.IsChecked()
config.conf["editableText"]["caretMoveTimeoutMs"]=self.caretMoveTimeoutSpinControl.GetValue()
Expand Down
26 changes: 18 additions & 8 deletions source/speech/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# -*- coding: UTF-8 -*-
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2006-2019 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler
# -*- coding: UTF-8 -*-
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler

"""High-level functions to speak information.
"""
Expand All @@ -23,6 +23,7 @@
import speechDictHandler
import characterProcessing
import languageHandler
from . import manager
from .commands import (
# Commands that are used in this file.
SpeechCommand,
Expand All @@ -37,6 +38,7 @@
# The following are imported here because other files that speech.py
# previously relied on "import * from .commands"
# New commands added to commands.py should be directly imported only where needed.
# Usage of these imports is deprecated and will be removed in 2021.1
SynthCommand,
IndexCommand,
SynthParamCommand,
Expand All @@ -51,6 +53,7 @@
ConfigProfileTriggerCommand,
)

from . import types
from .types import (
SpeechSequence,
SequenceItemT,
Expand All @@ -66,6 +69,7 @@
Generator,
Union,
Callable,
Iterator,
Tuple,
)
from logHandler import log
Expand Down Expand Up @@ -435,9 +439,14 @@ def getObjectPropertiesSpeech( # noqa: C901
newPropertyValues['states']=states
#Get the speech text for the properties we want to speak, and then speak it
speechSequence = getPropertiesSpeech(reason=reason, **newPropertyValues)

if speechSequence:
if _prefixSpeechCommand is not None:
speechSequence.insert(0, _prefixSpeechCommand)
from eventHandler import _getFocusLossCancellableSpeechCommand
cancelCommand = _getFocusLossCancellableSpeechCommand(obj, reason)
if cancelCommand is not None:
speechSequence.append(cancelCommand)
return speechSequence


Expand Down Expand Up @@ -472,6 +481,8 @@ def speakObject(
speak(sequence, priority=priority)




# C901 'getObjectSpeech' is too complex
# Note: when working on getObjectSpeech, look for opportunities to simplify
# and move logic out into smaller helper functions.
Expand Down Expand Up @@ -2544,10 +2555,9 @@ def _getSpeech(
#: Kept for backwards compatibility.
re_last_pause = _speakWithoutPauses.re_last_pause

from .manager import SpeechManager
#: The singleton _SpeechManager instance used for speech functions.
#: @type: L{_SpeechManager}
_manager = SpeechManager()
#: @type: L{manager.SpeechManager}
_manager = manager.SpeechManager()


def clearTypedWordBuffer() -> None:
Expand Down
Loading