Skip to content

Commit f4c73fa

Browse files
authored
Merge 6158795 into 0c0d88a
2 parents 0c0d88a + 6158795 commit f4c73fa

30 files changed

Lines changed: 114622 additions & 22 deletions

source/braille.py

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import extensionPoints
3737
import hwPortUtils
3838
import bdDetect
39-
import winUser
39+
import brailleViewer
4040
import queueHandler
4141

4242
roleLabels = {
@@ -1205,7 +1205,6 @@ def _get_regionsWithPositions(self):
12051205
yield RegionWithPositions(region, start, end)
12061206
start = end
12071207

1208-
_cache_rawToBraillePos=True
12091208
def _get_rawToBraillePos(self):
12101209
"""@return: a list mapping positions in L{rawText} to positions in L{brailleCells} for the entire buffer.
12111210
@rtype: [int, ...]
@@ -1215,7 +1214,8 @@ def _get_rawToBraillePos(self):
12151214
rawToBraillePos.extend(p+regionStart for p in region.rawToBraillePos)
12161215
return rawToBraillePos
12171216

1218-
_cache_brailleToRawPos=True
1217+
brailleToRawPos: List[int]
1218+
12191219
def _get_brailleToRawPos(self):
12201220
"""@return: a list mapping positions in L{brailleCells} to positions in L{rawText} for the entire buffer.
12211221
@rtype: [int, ...]
@@ -1250,7 +1250,25 @@ def regionPosToBufferPos(self, region, pos, allowNearest=False):
12501250
raise LookupError("No such position")
12511251

12521252
def bufferPositionsToRawText(self, startPos, endPos):
1253-
return self.rawText[self.brailleToRawPos[startPos]:self.brailleToRawPos[endPos-1]+1]
1253+
brailleToRawPos = self.brailleToRawPos
1254+
if not brailleToRawPos or not self.rawText:
1255+
# if either are empty, just return an empty string.
1256+
return ""
1257+
try:
1258+
lastIndex = len(brailleToRawPos) - 1
1259+
rawTextStart = brailleToRawPos[min(lastIndex, startPos)]
1260+
rawTextEnd = brailleToRawPos[min(lastIndex, endPos)] + 1
1261+
lastIndex = len(self.rawText)
1262+
return self.rawText[rawTextStart:min(lastIndex, rawTextEnd)]
1263+
except IndexError:
1264+
log.debugWarning(
1265+
f"Unable to get raw text for buffer positions"
1266+
f"(startPos-endPos): {startPos}-{endPos}, "
1267+
f"for rawText: {self.rawText}, "
1268+
f"with brailleToRawPos: {brailleToRawPos}",
1269+
exc_info=True
1270+
)
1271+
return ""
12541272

12551273
def bufferPosToWindowPos(self, bufferPos):
12561274
if not (self.windowStartPos <= bufferPos < self.windowEndPos):
@@ -1589,14 +1607,13 @@ class BrailleHandler(baseObject.AutoPropertyObject):
15891607
def __init__(self):
15901608
louisHelper.initialize()
15911609
self.display: Optional[BrailleDisplayDriver] = None
1592-
self.displaySize = 0
1610+
#: Number of cells the connected device (or if no device connected, what braille viewer has)
1611+
#: Zero cells disables braille. See L{_get_enabled}
1612+
self._displaySize: int = 0
15931613
self.mainBuffer = BrailleBuffer(self)
15941614
self.messageBuffer = BrailleBuffer(self)
15951615
self._messageCallLater = None
15961616
self.buffer = self.mainBuffer
1597-
#: Whether braille is enabled.
1598-
#: @type: bool
1599-
self.enabled = False
16001617
self._keyCountForLastMessage=0
16011618
self._cursorPos = None
16021619
self._cursorBlinkUp = True
@@ -1606,6 +1623,8 @@ def __init__(self):
16061623
self._tether = config.conf["braille"]["tetherTo"]
16071624
self._detectionEnabled = False
16081625
self._detector = None
1626+
self._rawText = u""
1627+
brailleViewer.postBrailleViewerToolToggledAction.register(self._onBrailleViewerChangedState)
16091628

16101629
def terminate(self):
16111630
bgThreadStopTimeout = 2.5 if self._detectionEnabled else None
@@ -1639,6 +1658,24 @@ def setTether(self, tether, auto=False):
16391658
def _get_shouldAutoTether(self):
16401659
return self.enabled and config.conf["braille"]["autoTether"]
16411660

1661+
displaySize: int
1662+
1663+
def _get_displaySize(self):
1664+
if self._displaySize == 0 and brailleViewer.isBrailleViewerActive():
1665+
return brailleViewer.DEFAULT_NUM_CELLS
1666+
return self._displaySize
1667+
1668+
def _set_displaySize(self, numCells):
1669+
"""The display size can be changed while a display is connected, for instance
1670+
see L{brailleDisplayDrivers.alva.BrailleDisplayDriver} split point feature.
1671+
"""
1672+
self._displaySize = numCells
1673+
1674+
enabled: bool
1675+
1676+
def _get_enabled(self):
1677+
return bool(self.displaySize)
1678+
16421679
_lastRequestedDisplayName=None #: the name of the last requested braille display driver with setDisplayByName, even if it failed and has fallen back to no braille.
16431680
def setDisplayByName(self, name, isFallback=False, detected=None):
16441681
if not isFallback:
@@ -1666,13 +1703,14 @@ def setDisplayByName(self, name, isFallback=False, detected=None):
16661703

16671704
try:
16681705
newDisplay = _getDisplayDriver(name)
1706+
oldDisplay = self.display
16691707
if detected and bdDetect._isDebug():
16701708
log.debug("Possibly detected display '%s'" % newDisplay.description)
1671-
if newDisplay == self.display.__class__:
1709+
if newDisplay == oldDisplay.__class__:
16721710
# This is the same driver as was already set, so just re-initialise it.
16731711
log.debug("Reinitializing %s braille display"%name)
1674-
self.display.terminate()
1675-
newDisplay = self.display
1712+
oldDisplay.terminate()
1713+
newDisplay = oldDisplay
16761714
try:
16771715
newDisplay.__init__(**kwargs)
16781716
except TypeError:
@@ -1697,8 +1735,7 @@ def setDisplayByName(self, name, isFallback=False, detected=None):
16971735
log.error("Error terminating previous display driver", exc_info=True)
16981736
self.display = newDisplay
16991737
newDisplay.initSettings()
1700-
self.displaySize = newDisplay.numCells
1701-
self.enabled = bool(self.displaySize)
1738+
self._displaySize = newDisplay.numCells
17021739
if isFallback:
17031740
if self._detectionEnabled and not self._detector:
17041741
# As this is the fallback display, which is usually noBraille,
@@ -1723,6 +1760,11 @@ def setDisplayByName(self, name, isFallback=False, detected=None):
17231760
self.setDisplayByName("noBraille", isFallback=True)
17241761
return False
17251762

1763+
def _onBrailleViewerChangedState(self, created):
1764+
if created:
1765+
self._updateDisplay()
1766+
log.debug("Braille Viewer enabled: {}".format(self.enabled))
1767+
17261768
def _updateDisplay(self):
17271769
if self._cursorBlinkTimer:
17281770
self._cursorBlinkTimer.Stop()
@@ -1740,6 +1782,7 @@ def _updateDisplay(self):
17401782
wx.CallAfter(self._cursorBlinkTimer.Start,blinkRate)
17411783

17421784
def _writeCells(self, cells):
1785+
brailleViewer.update(cells, self._rawText)
17431786
if not self.display.isThreadSafe:
17441787
try:
17451788
self.display.display(cells)
@@ -1775,6 +1818,7 @@ def _blink(self):
17751818

17761819
def update(self):
17771820
cells = self.buffer.windowBrailleCells
1821+
self._rawText = self.buffer.windowRawText
17781822
if log.isEnabledFor(log.IO):
17791823
log.io("Braille window dots: %s" % formatCellsForLog(cells))
17801824
# cells might not be the full length of the display.

source/brailleViewer/__init__.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# brailleViewer.py
2+
# A part of NonVisual Desktop Access (NVDA)
3+
# Copyright (C) 2014-2019 NV Access Limited
4+
# This file is covered by the GNU General Public License.
5+
# See the file COPYING for more details.
6+
from typing import Optional, List
7+
8+
import gui
9+
import extensionPoints
10+
from .brailleViewerGui import BrailleViewerFrame
11+
12+
"""
13+
### Overview
14+
This package contains the components for a "Braille Viewer". A window, that shows the braille dots that
15+
would be displayed on a hardware device. The raw text for each cell is also shown.
16+
This tool consists of:
17+
- A GUI for the viewer.
18+
- Construction / destruction / update helpers.
19+
20+
The current intention is to be able to support a physical braille device while using the "Braille Viewer".
21+
Due to limitations in the design of brailleHandler, the number of cells in the "Braille Viewer" must match any
22+
connected physical device.
23+
24+
### Life-cycle
25+
- Constructing / showing the BrailleViewer
26+
- On startup via L{core.doStartupDialogs}
27+
- Via NVDA (tools) menu via L{Mainframe.onToggleSpeechViewerCommand}
28+
- Hiding / destroying the BrailleViewer
29+
- On exit of NVDA.
30+
- Via NVDA (tools) menu via L{Mainframe.onToggleSpeechViewerCommand}
31+
- When the Window receives a close event. This means the GUI must be able to call-back to clean up
32+
BrailleHandler and the NVDA tools menu. This callback happens via the L{postBrailleViewerToolToggledAction}
33+
34+
### Number of cells shown
35+
The default (40) is set in L{createBrailleViewerTool}.
36+
37+
### Routing
38+
Currently not supported.
39+
In order to support routing the user must be able to click on the cells. This means that the BrailleViewer
40+
window gains focus, and the braille values are changed. To avoid this would require substantial changes to
41+
brailleHandler.
42+
43+
### Scrolling
44+
Scrolling is supported by binding a gesture to the braille_scroll_forward and braille scroll_back commands.
45+
For the same reason that Routing is not supported, scrolling via button clicks on the braille viewer window
46+
is not supported.
47+
48+
"""
49+
50+
# global braille viewer driver:
51+
_brailleGui: Optional[BrailleViewerFrame] = None
52+
53+
# Extension points action:
54+
# Triggered every time the Braille Viewer is created / shown or hidden / destroyed.
55+
# Callback definition: Callable(created: bool) -> None
56+
# created - True for created/shown, False for hidden/destructed.
57+
postBrailleViewerToolToggledAction = extensionPoints.Action()
58+
# Devices with 40 cells are quite common.
59+
DEFAULT_NUM_CELLS = 40
60+
61+
62+
def isBrailleViewerActive() -> bool:
63+
return bool(_brailleGui)
64+
65+
66+
def update(cells: List[int], rawText: str):
67+
if _brailleGui:
68+
_brailleGui.updateBrailleDisplayed(
69+
cells,
70+
rawText,
71+
_getDisplaySize()
72+
)
73+
74+
75+
def _destroyGUI():
76+
global _brailleGui
77+
d: Optional[BrailleViewerFrame] = _brailleGui
78+
_brailleGui = None
79+
if d and not d.isDestroyed:
80+
d.Destroy()
81+
82+
83+
def destroyBrailleViewer():
84+
_destroyGUI()
85+
postBrailleViewerToolToggledAction.notify(created=False)
86+
87+
88+
def _onGuiDestroyed():
89+
""" Used as a callback from L{BrailleViewerFrame}, lets us know that the GUI initiated a destruction.
90+
"""
91+
global _brailleGui
92+
if _brailleGui:
93+
# Destruction wasn't initiated from L{_destroyGUI} ie not through direct user action.
94+
# It's likely that the window received a shutdown event that could not be skipped.
95+
# Continue to destroy the braille viewer.
96+
destroyBrailleViewer()
97+
98+
99+
def _getDisplaySize():
100+
import braille # imported late to avoid a circular import.
101+
numCells = braille.handler.displaySize
102+
return numCells if numCells > 0 else DEFAULT_NUM_CELLS
103+
104+
105+
def createBrailleViewerTool():
106+
if not gui.mainFrame:
107+
raise RuntimeError("Can not initialise the BrailleViewerGui: gui.mainFrame not yet initialised")
108+
109+
import braille # imported late to avoid a circular import.
110+
if not braille.handler:
111+
raise RuntimeError("Can not initialise the BrailleViewerGui: braille.handler not yet initialised")
112+
113+
global _brailleGui
114+
if _brailleGui:
115+
_destroyGUI()
116+
117+
_brailleGui = BrailleViewerFrame(
118+
_getDisplaySize(),
119+
_onGuiDestroyed
120+
)
121+
122+
postBrailleViewerToolToggledAction.notify(created=True)

0 commit comments

Comments
 (0)