Skip to content

Commit

Permalink
Options for focus context presentation in braille. Incubates #7236 (i…
Browse files Browse the repository at this point in the history
…ssue #217).
  • Loading branch information
jcsteh committed Jul 19, 2017
2 parents a2a538f + 7aa3631 commit 6c68b5f
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 15 deletions.
91 changes: 81 additions & 10 deletions source/braille.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#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) 2008-2017 NV Access Limited, Joseph Lee
#Copyright (C) 2008-2017 NV Access Limited, Joseph Lee, Babbage B.V.

import sys
import itertools
Expand All @@ -25,6 +25,7 @@
import brailleDisplayDrivers
import inputCore
import brailleTables
from collections import namedtuple

roleLabels = {
# Translators: Displayed in braille for an object which is an
Expand Down Expand Up @@ -135,6 +136,33 @@
# used to separate chunks of text when programmatically joined
TEXT_SEPARATOR = " "

#: Identifier for a focus context presentation setting that
#: only shows as much as possible focus context information when the context has changed.
CONTEXTPRES_CHANGEDCONTEXT = "changedContext"
#: Identifier for a focus context presentation setting that
#: shows as much as possible focus context information if the focus object doesn't fill up the whole display.
CONTEXTPRES_FILL = "fill"
#: Identifier for a focus context presentation setting that
#: always shows the object with focus at the very left of the braille display.
CONTEXTPRES_SCROLL = "scroll"
#: Focus context presentations associated with their user readable and translatable labels
focusContextPresentations=[
# Translators: The label for a braille focus context presentation setting that
# only shows as much as possible focus context information when the context has changed.
(CONTEXTPRES_CHANGEDCONTEXT, _("Fill display for context changes")),
# Translators: The label for a braille focus context presentation setting that
# shows as much as possible focus context information if the focus object doesn't fill up the whole display.
# This was the pre NVDA 2017.3 default.
(CONTEXTPRES_FILL, _("Always fill display")),
# Translators: The label for a braille focus context presentation setting that
# always shows the object with focus at the very left of the braille display
# (i.e. you will have to scroll back for focus context information).
(CONTEXTPRES_SCROLL, _("Only when scrolling back")),
]

#: Named tuple for a region with start and end positions in a buffer
RegionWithPositions = namedtuple("RegionWithPositions",("region","start","end"))

def NVDAObjectHasUsefulText(obj):
import displayModel
if issubclass(obj.TextInfo,displayModel.DisplayModelTextInfo):
Expand Down Expand Up @@ -778,8 +806,12 @@ def update(self):
# If this is not the start of the object, hide all previous regions.
start = readingInfo.obj.makeTextInfo(textInfos.POSITION_FIRST)
self.hidePreviousRegions = (start.compareEndPoints(readingInfo, "startToStart") < 0)
# If this is a multiline control, position it at the absolute left of the display when focused.
self.focusToHardLeft = self._isMultiline()
# Don't touch focusToHardLeft if it is already true
# For example, it can be set to True in getFocusContextRegions when this region represents the first new focus ancestor
# Alternatively, BrailleHandler._doNewObject can set this to True when this region represents the focus object and the focus ancestry didn't change
if not self.focusToHardLeft:
# If this is a multiline control, position it at the absolute left of the display when focused.
self.focusToHardLeft = self._isMultiline()
super(TextInfoRegion, self).update()

if rawInputIndStart is not None:
Expand Down Expand Up @@ -935,7 +967,7 @@ def _get_regionsWithPositions(self):
start = 0
for region in self.visibleRegions:
end = start + len(region.brailleCells)
yield region, start, end
yield RegionWithPositions(region, start, end)
start = end

def bufferPosToRegionPos(self, bufferPos):
Expand Down Expand Up @@ -982,12 +1014,27 @@ def _get_windowEndPos(self):
return endPos

def _set_windowEndPos(self, endPos):
"""Sets the end position for the braille window and recalculates the window start position based on several variables.
1. Braille display size.
2. Whether one of the regions should be shown hard left on the braille display;
i.e. because of The configuration setting for focus context representation
or whether the braille region that corresponds with the focus represents a multi line edit box.
3. Whether word wrap is enabled."""
startPos = endPos - self.handler.displaySize
# Get the last region currently displayed.
region, regionPos = self.bufferPosToRegionPos(endPos - 1)
if region.focusToHardLeft:
# Only scroll to the start of this region.
restrictPos = endPos - regionPos - 1
# Loop through the currently displayed regions in reverse order
# If focusToHardLeft is set for one of the regions, the display shouldn't scroll further back than the start of that region
for region, regionStart, regionEnd in reversed(list(self.regionsWithPositions)):
if regionStart<=endPos:
if region.focusToHardLeft:
# Only scroll to the start of this region.
restrictPos = regionStart
break
elif config.conf["braille"]["focusContextPresentation"]!=CONTEXTPRES_CHANGEDCONTEXT:
# We aren't currently dealing with context change presentation
# thus, we only need to consider the last region
# since it doesn't have focusToHardLeftSet, the window start position isn't restricted
restrictPos = 0
break
else:
restrictPos = 0
if startPos <= restrictPos:
Expand Down Expand Up @@ -1060,7 +1107,7 @@ def focus(self, region):
"""
pos = self.regionPosToBufferPos(region, 0)
self.windowStartPos = pos
if region.focusToHardLeft:
if region.focusToHardLeft or config.conf["braille"]["focusContextPresentation"]==CONTEXTPRES_SCROLL:
return
end = self.windowEndPos
if end - pos < self.handler.displaySize:
Expand Down Expand Up @@ -1177,16 +1224,28 @@ def getFocusContextRegions(obj, oldFocusRegions=None):
newAncestorsStart = 1
# Yield the common regions.
for region in oldFocusRegions[0:commonRegionsEnd]:
# We are setting focusToHardLeft to False for every cached region.
# This is necessary as BrailleHandler._doNewObject checks focusToHardLeft on every region
# and sets it to True for the first focus region if the context didn't change.
# If we don't do this, BrailleHandler._doNewObject can't set focusToHardLeft properly.
region.focusToHardLeft = False
yield region
else:
# Fetch all ancestors.
newAncestorsStart = 1

focusToHardLeftSet = False
for index, parent in enumerate(ancestors[newAncestorsStart:ancestorsEnd], newAncestorsStart):
if not parent.isPresentableFocusAncestor:
continue
region = NVDAObjectRegion(parent, appendText=TEXT_SEPARATOR)
region._focusAncestorIndex = index
if config.conf["braille"]["focusContextPresentation"]==CONTEXTPRES_CHANGEDCONTEXT and not focusToHardLeftSet:
# We are presenting context changes to the user
# Thus, only scroll back as far as the start of the first new focus ancestor
# focusToHardLeftSet is used since the first new ancestor isn't always represented by a region
region.focusToHardLeft = True
focusToHardLeftSet = True
region.update()
yield region

Expand Down Expand Up @@ -1457,7 +1516,19 @@ def handleGainFocus(self, obj):

def _doNewObject(self, regions):
self.mainBuffer.clear()
focusToHardLeftSet = False
for region in regions:
if self.tether == self.TETHER_FOCUS and config.conf["braille"]["focusContextPresentation"]==CONTEXTPRES_CHANGEDCONTEXT:
# Check focusToHardLeft for every region.
# If noone of the regions has focusToHardLeft set to True, set it for the first focus region.
if region.focusToHardLeft:
focusToHardLeftSet = True
elif not focusToHardLeftSet and getattr(region, "_focusAncestorIndex", None) is None:
# Going to display a new object with the same ancestry as the previously displayed object.
# So, set focusToHardLeft on this region
# For example, this applies when you are in a list and start navigating through it
region.focusToHardLeft = True
focusToHardLeftSet = True
self.mainBuffer.regions.append(region)
self.mainBuffer.update()
# Last region should receive focus.
Expand Down
3 changes: 2 additions & 1 deletion source/config/configSpec.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: UTF-8 -*-
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2006-2017 NV Access Limited
#Copyright (C) 2006-2017 NV Access Limited, Babbage B.V.
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.

Expand Down Expand Up @@ -64,6 +64,7 @@
tetherTo = string(default="focus")
readByParagraph = boolean(default=false)
wordWrap = boolean(default=true)
focusContextPresentation = option("changedContext", "fill", "scroll", default="changedContext")
# Braille display driver settings
[[__many__]]
Expand Down
19 changes: 19 additions & 0 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,25 @@ def script_braille_toggleTether(self, gesture):
script_braille_toggleTether.__doc__ = _("Toggle tethering of braille between the focus and the review position")
script_braille_toggleTether.category=SCRCAT_BRAILLE

def script_braille_toggleFocusContextPresentation(self, gesture):
values = [x[0] for x in braille.focusContextPresentations]
labels = [x[1] for x in braille.focusContextPresentations]
try:
index = values.index(config.conf["braille"]["focusContextPresentation"])
except:
index=0
newIndex = (index+1) % len(values)
config.conf["braille"]["focusContextPresentation"] = values[newIndex]
braille.invalidateCachedFocusAncestors(0)
braille.handler.handleGainFocus(api.getFocusObject())
# Translators: Reports the new state of braille focus context presentation.
# %s will be replaced with the context presentation setting.
# For example, the full message might be "Braille focus context presentation: fill display for context changes"
ui.message(_("Braille focus context presentation: %s")%labels[newIndex].lower())
# Translators: Input help mode message for toggle braille focus context presentation command.
script_braille_toggleFocusContextPresentation.__doc__ = _("Toggle the way context information is presented in braille")
script_braille_toggleFocusContextPresentation.category=SCRCAT_BRAILLE

def script_braille_toggleShowCursor(self, gesture):
if config.conf["braille"]["showCursor"]:
# Translators: The message announced when toggling the braille cursor.
Expand Down
13 changes: 12 additions & 1 deletion source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: UTF-8 -*-
#settingsDialogs.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2006-2017 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Rui Batista, Joseph Lee, Heiko Folkerts, Zahari Yurukov, Leonard de Ruijter, Derek Riemer
#Copyright (C) 2006-2017 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Rui Batista, Joseph Lee, Heiko Folkerts, Zahari Yurukov, Leonard de Ruijter, Derek Riemer, Babbage B.V.
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.

Expand Down Expand Up @@ -1692,6 +1692,16 @@ def makeSettings(self, settingsSizer):
wordWrapText = _("Avoid splitting &words when possible")
self.wordWrapCheckBox = sHelper.addItem(wx.CheckBox(self, label=wordWrapText))
self.wordWrapCheckBox.Value = config.conf["braille"]["wordWrap"]
# Translators: The label for a setting in braille settings to select how the context for the focus object should be presented on a braille display.
focusContextPresentationLabelText = _("Focus context presentation:")
self.focusContextPresentationValues = [x[0] for x in braille.focusContextPresentations]
focusContextPresentationChoices = [x[1] for x in braille.focusContextPresentations]
self.focusContextPresentationList = sHelper.addLabeledControl(focusContextPresentationLabelText, wx.Choice, choices=focusContextPresentationChoices)
try:
index=self.focusContextPresentationValues.index(config.conf["braille"]["focusContextPresentation"])
except:
index=0
self.focusContextPresentationList.SetSelection(index)

def postInit(self):
self.displayList.SetFocus()
Expand Down Expand Up @@ -1719,6 +1729,7 @@ def onOk(self, evt):
braille.handler.tether = self.tetherValues[self.tetherList.GetSelection()][0]
config.conf["braille"]["readByParagraph"] = self.readByParagraphCheckBox.Value
config.conf["braille"]["wordWrap"] = self.wordWrapCheckBox.Value
config.conf["braille"]["focusContextPresentation"] = self.focusContextPresentationValues[self.focusContextPresentationList.GetSelection()]
super(BrailleSettingsDialog, self).onOk(evt)

def onDisplayNameChanged(self, evt):
Expand Down
9 changes: 6 additions & 3 deletions tests/unit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,17 @@ class AppArgs:
# Anything which notifies of cursor updates requires braille to be initialized.
import braille
braille.initialize()
# For braille unit tests, we need to construct a fake braille display as well as enable the braille handler
# Give the display 40 cells
braille.handler.displaySize=40
braille.handler.enabled = True
# The focus and navigator objects need to be initialized to something.
from NVDAObjects import NVDAObject
class PlaceholderNVDAObject(NVDAObject):
processID = None # Must be implemented to instantiate.
from objectProvider import PlaceholderNVDAObject,NVDAObjectWithRole
phObj = PlaceholderNVDAObject()
import api
api.setFocusObject(phObj)
api.setNavigatorObject(phObj)
api.setDesktopObject(phObj)

# Stub speech functions to make them no-ops.
# Eventually, these should keep track of calls so we can make assertions.
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/objectProvider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#tests/unit/objectProvider.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) 2017 NV Access Limited, Babbage B.V.

"""Fake object provider implementation for testing of code which uses NVDAObjects.
"""

from NVDAObjects import NVDAObject
import controlTypes

class PlaceholderNVDAObject(NVDAObject):
processID = None # Must be implemented to instantiate.

class NVDAObjectWithRole(PlaceholderNVDAObject):
"""An object that accepts a role as one of its construction parameters.
The name of the object will be set with the associated role label.
This class can be used to quickly create objects for a fake focus ancestry."""

def __init__(self, role=controlTypes.ROLE_UNKNOWN,**kwargs):
super(NVDAObjectWithRole,self).__init__(**kwargs)
self.role=role

def _get_name(self):
return controlTypes.roleLabels.get(self.role,controlTypes.ROLE_UNKNOWN)

84 changes: 84 additions & 0 deletions tests/unit/test_braille.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#tests/unit/test_braille.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) 2017 NV Access Limited, Babbage B.V.

"""Unit tests for the braille module.
"""

import unittest
import braille
from objectProvider import PlaceholderNVDAObject, NVDAObjectWithRole
import controlTypes
from config import conf
import api
import globalVars

class TestFocusContextPresentation(unittest.TestCase):
"""A test for the different focus context presentation options."""

@property
def regionsWithPositions(self):
return list(braille.handler.buffer.regionsWithPositions)

def setUp(self):
"""Set up a fake focus object and give it some ancestry."""
self.obj=NVDAObjectWithRole(role=controlTypes.ROLE_LISTITEM)
# Forcefully create a fake focus ancestry
# Note that the braille code excludes the desktop object when getting regions for focus ancestry
# The resulting focus object including ancestry will look like: "dialog dlg list lst list item"
globalVars.focusAncestors=[api.getDesktopObject(),NVDAObjectWithRole(role=controlTypes.ROLE_DIALOG),NVDAObjectWithRole(role=controlTypes.ROLE_LIST)]
braille.handler.handleGainFocus(self.obj)
# Make sure that we are testing with three regions
self.assertEqual(len(self.regionsWithPositions),3)

def test_fillDisplay(self):
"""Test for the case where both the focus object and all its ancestors should be visible on a 40 cell display."""
conf['braille']['focusContextPresentation']=braille.CONTEXTPRES_FILL
# Since we set the presentation mode, simulate another gainFocus so the regions will be updated properly
braille.handler.handleGainFocus(self.obj)
# WindowEndPos should be retrieved before we attempt to get the start position
# This is because getting windowEndPos can update windowStartPos
# Both the focus object and its ancestors should fit on the display
# Thus, the window end position is equal to the end position of the 3rd region
self.assertEqual(braille.handler.buffer.windowEndPos,self.regionsWithPositions[2].end)
# The start position should be 0 now
self.assertEqual(braille.handler.buffer.windowStartPos,0)

def test_scrollOnly(self):
"""Test for the case where the focus object should be visible hard left on a display."""
conf['braille']['focusContextPresentation']=braille.CONTEXTPRES_SCROLL
braille.handler.handleGainFocus(self.obj)
# Only the focus object should be visible on the display
# This means that the window end position is equal to the end position of the 3rd region
self.assertEqual(braille.handler.buffer.windowEndPos,self.regionsWithPositions[2].end)
# This also means that the window start position is equal to the start position of the 3rd region
self.assertEqual(braille.handler.buffer.windowStartPos,self.regionsWithPositions[2].start)

def test_changedContext(self):
"""Test for the case where the focus object as well as ancestry differences should be visible on the display"""
conf['braille']['focusContextPresentation']=braille.CONTEXTPRES_CHANGEDCONTEXT
# Clean up the cached ancestry regions
braille.invalidateCachedFocusAncestors(0)
# Regenerate the regions
braille.handler.handleGainFocus(self.obj)
# Both the focus object and its parents should be visible, equivalent to always fill display
self.assertEqual(braille.handler.buffer.windowEndPos,self.regionsWithPositions[2].end)
self.assertEqual(braille.handler.buffer.windowStartPos,0)
# Do another focus to simulate a new focus object with equal ancestry
braille.handler.handleGainFocus(self.obj)
# Only the focus object should be visible now, equivalent to scroll only
self.assertEqual(braille.handler.buffer.windowEndPos,self.regionsWithPositions[2].end)
self.assertEqual(braille.handler.buffer.windowStartPos,self.regionsWithPositions[2].start)
# Clean up the cached focus ancestors
# specifically, the desktop object (ancestor 0) has no associated region
# We will keep the region for the dialog (ancestor 1) and consider the list (ancestor 2) as new for this test
braille.invalidateCachedFocusAncestors(2)
# Do another focus to simulate a new focus object with different ancestry
braille.handler.handleGainFocus(self.obj)
# The list and the list item should be visible
# This still means that the window end position is equal to the end position of the 3rd region
self.assertEqual(braille.handler.buffer.windowEndPos,self.regionsWithPositions[2].end)
# The window start position is equal to the start position of the 2nd region
self.assertEqual(braille.handler.buffer.windowStartPos,self.regionsWithPositions[1].start)
Loading

0 comments on commit 6c68b5f

Please sign in to comment.