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

Add a bunch of extension points to eliminate NVDA Remote monkey patching #7594

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
90 changes: 83 additions & 7 deletions source/braille.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@
)
SELECTION_SHAPE = 0xC0 #: Dots 7 and 8

#: The braille shape shown on a braille display when
#: the number of cells used by the braille handler is lower than the actual number of cells.
#: The 0 based position of the shape is equal to the number of cells used by the braille handler.
END_OF_BRAILLE_OUTPUT_SHAPE = 0xFF #: All dots

#: Unicode braille indicator at the start of untranslated braille input.
INPUT_START_IND = u"⣏"
#: Unicode braille indicator at the end of untranslated braille input.
Expand Down Expand Up @@ -1556,14 +1561,10 @@ class BrailleHandler(baseObject.AutoPropertyObject):

def __init__(self):
self.display = None
self.displaySize = 0
self.mainBuffer = BrailleBuffer(self)
self.messageBuffer = BrailleBuffer(self)
self._messageCallLater = None
self.buffer = self.mainBuffer
#: Whether braille is enabled.
#: @type: bool
self.enabled = False
self._keyCountForLastMessage=0
self._cursorPos = None
self._cursorBlinkUp = True
Expand All @@ -1574,6 +1575,62 @@ def __init__(self):
self._detectionEnabled = False
self._detector = None

#: Filter that allows components or add-ons to filter or change the raw cell data,
#: that is written to a braille display.
#: For example, the Handy Tech ATC functionality doesn't support empty lines,
#: and therefore, empty lines should at least contain some filled cells.
#: @param value: The list of braille cells.
#: @type value: [int]
self.filter_writeCells = extensionPoints.Filter()

#: Notifies when cells are about to be written to a braille display.
#: This allows components and add-ons to perform an action.
#: For example, when a system is controlled by a braille enabled remote system,
#: the remote system should know what cells to show on its display.
#: @param cells: The list of braille cells.
#: @type cells: [int]
self.pre_writeCells = extensionPoints.Action()

#: Filter that allows components or add-ons to change the display size used for braille output.
#: For example, when a system is controlled by a remote system while having a 80 cells display connected,
#: the display size should be lowered to 40 whenever the remote system has a 40 cells display connected.
#: @param value: the number of cells of the current display.
#: @type value: int
self.filter_displaySize = extensionPoints.Filter()

#: Allows components or add-ons to decide whether the braille handler should be forcefully disabled.
#: For example, when a system is controlling a remote system with braille,
#: the local braille handler should be disabled as long as the system is in control of the remote system.
#: Handlers are called without arguments.
self.decide_enabled = extensionPoints.Decider()

_cache_displaySize = True
def _get_displaySize(self):
"""Returns the display size to use for braille output.
Handlers can register themselves to L{filter_displaySize} to change this value on the fly.
Therefore, this is a read only property and can't be set.
"""
numCells = self.display.numCells if self.display else 0
return self.filter_displaySize.apply(numCells)

def _set_displaySize(self, value):
raise AttributeError("Can't set displaySize to %r, consider registering a handler to filter_displaySize" % value)

_cache_enabled = True
def _get_enabled(self):
"""Returns whether braille is enabled.
Handlers can register themselves to L{decide_enabled} and return C{False} to forcefully disable the braille handler.
If components need to change the state from disabled to enabled instead, they should register to L{filter_displaySize}.
By default, the enabled/disabled state is based on the boolean value of L{displaySize},
and thus is C{True} when the display size is greater than 0.
This is a read only property and can't be set.
@rtype: bool
"""
return bool(self.displaySize) and self.decide_enabled.decide()

def _set_enabled(self, value):
raise AttributeError("Can't set enabled to %r, consider registering a handler to decide_enabled or filter_displaySize" % value)

def terminate(self):
bgThreadStopTimeout = 2.5 if self._detectionEnabled else None
self._disableDetection()
Expand Down Expand Up @@ -1670,15 +1727,13 @@ def setDisplayByName(self, name, isFallback=False, detected=None):
except:
log.error("Error terminating previous display driver", exc_info=True)
self.display = newDisplay
self.displaySize = newDisplay.numCells
self.enabled = bool(self.displaySize)
if isFallback:
self._resumeDetection()
elif not detected:
config.conf["braille"]["display"] = name
else: # detected:
self._disableDetection()
log.info("Loaded braille display driver %s, current display has %d cells." %(name, self.displaySize))
log.info("Loaded braille display driver %s, current display has %d cells." %(name, newDisplay.numCells))
self.initialDisplay()
return True
except:
Expand Down Expand Up @@ -1707,6 +1762,27 @@ def _updateDisplay(self):
wx.CallAfter(self._cursorBlinkTimer.Start,blinkRate)

def _writeCells(self, cells):
cells = self.filter_writeCells.apply(cells)
self.pre_writeCells.notify(cells=cells)
displayCellCount = self.display.numCells
handlerCellCount = self.displaySize
if not displayCellCount:
# No physical display to write to
return
# Braille displays expect cells to be padded up to displayCellCount.
# However, the braille handler uses handlerCellCount to calculate the number of cells.
cellCountDif = displayCellCount - len(cells)
if cellCountDif < 0:
# There are more cells than the connected display could take.
log.warning(
"Connected display %s has %d cells, while braille handler is using %d cells" %
(self.display.name, displayCellCount, handlerCellCount)
)
cells = cells[:displayCellCount]
elif cellCountDif > 0:
# The connected display could take more cells than the braille handler produces.
# Displays expect cells to be padded up to the number of cells.
cells += [END_OF_BRAILLE_OUTPUT_SHAPE] + [0]*cellCountDif
if not self.display.isThreadSafe:
try:
self.display.display(cells)
Expand Down
4 changes: 0 additions & 4 deletions source/brailleDisplayDrivers/alva.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ def _get_model(self):
return self.model

def _updateSettings(self):
oldNumCells = self.numCells
if self.isHid:
displaySettings = self._dev.getFeature(ALVA_DISPLAY_SETTINGS_REPORT)
if ord(displaySettings[ALVA_DISPLAY_SETTINGS_STATUS_CELL_SIDE_POS]) > 1:
Expand Down Expand Up @@ -146,9 +145,6 @@ def _updateSettings(self):
self._ser6SendMessage(b"H", b"?")
# Get HID keyboard input state
self._ser6SendMessage(b"r", b"?")
if oldNumCells not in (0, self.numCells):
# In case of splitpoint changes, we need to update the braille handler as well
braille.handler.displaySize = self.numCells

def __init__(self, port="auto"):
super(BrailleDisplayDriver,self).__init__()
Expand Down
18 changes: 17 additions & 1 deletion source/inputCore.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import languageHandler
import controlTypes
import keyLabels
import extensionPoints

#: Script category for emulated keyboard keys.
# Translators: The name of a category of NVDA commands.
Expand Down Expand Up @@ -208,7 +209,7 @@ def clear(self):
def add(self, gesture, module, className, script,replace=False):
"""Add a gesture mapping.
@param gesture: The gesture identifier.
@type gesture: str
@type gesture: L{InputGesture}
@param module: The name of the Python module containing the target script.
@type module: str
@param className: The name of the class in L{module} containing the target script.
Expand Down Expand Up @@ -403,6 +404,15 @@ def __init__(self):
self.loadLocaleGestureMap()
self.loadUserGestureMap()

#: Notifies when a gesture is about to be executed,
#: and allows components or add-ons to decide whether or not to execute a gesture.
#: For example, when controlling a remote system with a connected local braille display,
#: braille display gestures should not be executed locally.
#: Handlers are called with one argument:
#: @param gesture: The gesture that is about to be executed.
#: @type gesture: L{InputGesture}
self.decide_executeGesture = extensionPoints.Decider()

def executeGesture(self, gesture):
"""Perform the action associated with a gesture.
@param gesture: The gesture to execute.
Expand All @@ -415,6 +425,12 @@ def executeGesture(self, gesture):
# as well as stopping a flood of actions when the core revives.
raise NoInputGestureAction

if not self.decide_executeGesture.decide(gesture=gesture):
# A registered handler decided that this gesture shouldn't be executed.
# Purposely do not raise a NoInputGestureAction here, as that could lead to unexpected behavior for gesture emulation.
log.debug("Gesture execution canceled by handler registered to decide_executeGesture extension point")
return

script = gesture.script
focus = api.getFocusObject()
if focus.sleepMode is focus.SLEEP_FULL or (focus.sleepMode and not getattr(script, 'allowInSleepMode', False)):
Expand Down
26 changes: 23 additions & 3 deletions source/nvwave.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#nvwave.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2007-2017 NV Access Limited, Aleksey Sadovoy
#Copyright (C) 2007-2017 NV Access Limited, Aleksey Sadovoy, Babbage B.V.
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.

Expand All @@ -17,6 +17,7 @@
import wave
import config
from logHandler import log
import extensionPoints

__all__ = (
"WavePlayer", "getOutputDeviceNames", "outputDeviceIDToName", "outputDeviceNameToID",
Expand All @@ -27,6 +28,14 @@
HWAVEOUT = HANDLE
LPHWAVEOUT = POINTER(HWAVEOUT)

#: Notifies when a wave file is about to be played,
#: and allows components or add-ons to decide whether the wave file should be played.
#: For example, when controlling a remote system,
#: the remote system must be notified of sounds played on the local system.
#: Also, registrars should be able to suppress playing sounds if desired.
#: Handlers are called with the same arguments as L{playWaveFile} as keyword arguments.
decide_playWaveFile = extensionPoints.Decider()

class WAVEFORMATEX(Structure):
_fields_ = [
("wFormatTag", WORD),
Expand Down Expand Up @@ -329,16 +338,27 @@ def outputDeviceNameToID(name, useDefaultIfInvalid=False):

fileWavePlayer = None
fileWavePlayerThread=None
def playWaveFile(fileName, async=True):
def playWaveFile(fileName, async=True, partOfSpeechSequence=False):
"""plays a specified wave file.
"""
@param fileName: the path to the wave file, usually absolute.
@type fileName: basestring
@param async: whether the file should be played asynchronously.
If C{False}, the calling thread is blocked until the wave has finished playing.
@type async: bool
@param partOfSpeechSequence: whether this beep is created as part of a speech sequence.
@type partOfSpeechSequence: bool
"""
global fileWavePlayer, fileWavePlayerThread
f = wave.open(fileName,"r")
if f is None: raise RuntimeError("can not open file %s"%fileName)
if fileWavePlayer is not None:
fileWavePlayer.stop()
if not decide_playWaveFile.decide(fileName=fileName, async=async, partOfSpeechSequence=partOfSpeechSequence):
log.debug("Playing wave file canceled by handler registered to decide_playWaveFile extension point")
return
fileWavePlayer = WavePlayer(channels=f.getnchannels(), samplesPerSec=f.getframerate(),bitsPerSample=f.getsampwidth()*8, outputDevice=config.conf["speech"]["outputDevice"],wantDucking=False)
fileWavePlayer.feed(f.readframes(f.getnframes()))

if async:
if fileWavePlayerThread is not None:
fileWavePlayerThread.join()
Expand Down
28 changes: 21 additions & 7 deletions source/tones.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#tones.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2007-2017 NV Access Limited, Aleksey Sadovoy
#Copyright (C) 2007-2017 NV Access Limited, Aleksey Sadovoy, Babbage B.V.
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.

Expand All @@ -12,13 +12,14 @@
import globalVars
from logHandler import log
from ctypes import create_string_buffer, byref
import extensionPoints

SAMPLE_RATE = 44100

try:
player = nvwave.WavePlayer(channels=2, samplesPerSec=int(SAMPLE_RATE), bitsPerSample=16, outputDevice=config.conf["speech"]["outputDevice"],wantDucking=False)
except:
log.warning("Failed to initialize audio for tones")
log.warning("Failed to initialize audio for tones", exc_info=True)
player = None

# When exiting, ensure player is deleted before modules get cleaned up.
Expand All @@ -28,18 +29,31 @@ def _cleanup():
global player
player = None

def beep(hz,length,left=50,right=50):
#: Notifies when a beep is about to be generated and played,
#: and allows components or add-ons to decide whether the beep should actually be played.
#: For example, when controlling a remote system,
#: the remote system must be notified of beeps played on the local system.
#: Also, registrars should be able to suppress playing beeps if desired.
#: Handlers are called with the same arguments as L{beep} as keyword arguments.
decide_beep = extensionPoints.Decider()

def beep(hz,length,left=50,right=50,partOfSpeechSequence=False):
"""Plays a tone at the given hz, length, and stereo balance.
@param hz: pitch in hz of the tone
@param hz: pitch in hz of the tone.
@type hz: float
@param length: length of the tone in ms
@param length: length of the tone in ms.
@type length: integer
@param left: volume of the left channel (0 to 100)
@param left: volume of the left channel (0 to 100).
@type left: integer
@param right: volume of the right channel (0 to 100)
@param right: volume of the right channel (0 to 100).
@type right: integer
@param partOfSpeechSequence: whether this beep is created as part of a speech sequence.
@type partOfSpeechSequence: bool
"""
log.io("Beep at pitch %s, for %s ms, left volume %s, right volume %s"%(hz,length,left,right))
if not decide_beep.decide(hz=hz, length=length, left=left, right=right, partOfSpeechSequence=partOfSpeechSequence):
log.debug("Beep canceled by handler registered to decide_beep extension point")
return
if not player:
return
from NVDAHelper import generateBeep
Expand Down