Correctly follow selection in braille when extending beyond display width#6775
Conversation
…TreeInterceptor. The property defaults to False. Also use this to have braille scroll the selection's end into view if the anchor is at the start. re nvaccess#5770
- The copy of the braille cursor TextInfo in braille.TextInfoRegion.update(). - Duplicate call to detectPossibleSelectionChange(). This is now handled in EditableTextWithAutoSelectDetection.
…ensate for that. As a consequence, braille will always show the last selected character, both for single and multi-line selections.
| if eventHandler.isPendingEvents('valueChange',self): | ||
| self.hasContentChangedSinceLastSelection=True | ||
| super(Edit,self).event_caret() | ||
| self.detectPossibleSelectionChange() |
There was a problem hiding this comment.
For justification of this removal, see #5770 (comment)
| newInfo.setEndPoint(oldInfo, "startToStart") | ||
| # _isSelectionAnchoredAtStart is not the same as _lastSelectionMovedStart, | ||
| # i.e. their default values are opposite and _isSelectionAnchoredAtStart is only updated if the selection has changed. | ||
| self._isSelectionAnchoredAtStart = not self._lastSelectionMovedStart |
| speech.speakObject(self,reason=controlTypes.REASON_FOCUS) | ||
|
|
||
| def _get_isSelectionAnchoredAtStart(self): | ||
| """Determine if the selection is anchored at the start. |
There was a problem hiding this comment.
It might help here if you give a brief explanation of what you mean with a selection being anchored at the start.
| if eventHandler.isPendingEvents('valueChange',self): | ||
| self.hasContentChangedSinceLastSelection=True | ||
| super(Edit,self).event_caret() | ||
| self.detectPossibleSelectionChange() |
| def scrollToCursorOrSelection(self, region): | ||
| if region.brailleCursorPos is not None: | ||
| self.mainBuffer.scrollTo(region, region.brailleCursorPos) | ||
| elif not hasattr(region, "obj") or not region.obj.isSelectionAnchoredAtStart: |
There was a problem hiding this comment.
Probably a matter of style, but instead of the hasatr, I'd do a isinstance(region,(NVDAObjectRegion,TextInfoRegion)) here
| self.reportSelectionChange(oldInfo) | ||
| except: | ||
| return | ||
| return |
There was a problem hiding this comment.
I don't get how this one got into the diff, they look identical
|
@LeonarddeR I tweaked the pydoc. Also, I believe |
jcsteh
left a comment
There was a problem hiding this comment.
Thanks for your work. This looks pretty good overall.
| """ | ||
| speech.speakObject(self,reason=controlTypes.REASON_FOCUS) | ||
|
|
||
| def _get_isSelectionAnchoredAtStart(self): |
There was a problem hiding this comment.
Could you please rename this to isTextSelectionAnchoredAtStart? It occurred to me that selection might be a bit ambiguous here; i.e. selection of items in a container vs text selection.
|
|
||
| def _get_isSelectionAnchoredAtStart(self): | ||
| """Indicates if the selection is anchored at the start. | ||
| The anchored position is the end that doesn't move when extending or shrinking the selection. |
There was a problem hiding this comment.
Perhaps you could add an example here:
For example, if you have no selection and you press shift+rightArrow to select the next character, this will return True. In contrast, if you have no selection and you press shift+leftArrow to select the previous character, this will return False.
| If the selection is anchored at the end or there is no information this is C{False}. | ||
| @return: C{True} if the selection is anchored at the start else C{False} | ||
| @rtype: bool | ||
| """ |
There was a problem hiding this comment.
It strikes me as odd that we create this private variable here which gets set in a completely different class. That is, when you're reading this code, it's not clear where this gets set. I'm thinking the base implementation should just be a class variable with a default value and then the classes which implement this can either implement their own property or override with a simple instance variable.
| # Collapse the selection to the point that is moving. | ||
| readingInfo.collapse(end=self.obj.isSelectionAnchoredAtStart) | ||
| # Get the reading unit at the end of the selection. | ||
| readingInfo.expand(unit) |
There was a problem hiding this comment.
As noted in the comment that was previously here:
# HACK: Some TextInfos only support UNIT_LINE properly if they are based on POSITION_CARET, # so use the original cursor TextInfo for line and copy for cursor.
I think this is going to break for certain TextInfos. Affected implementations will only let you get the line if you're working with the caret or selection range. Once you copy the range, that ability is lost. This used to be true for Word, though I think we've since rectified that. However, I'm pretty certain this is still true for MSHTML. Long story short: let readingInfo be the TextInfo you create from POSITION_SELECTION and then copy it for sel.
| newInfo.setEndPoint(oldInfo, "startToStart") | ||
| # _isSelectionAnchoredAtStart is not the same as _lastSelectionMovedStart, | ||
| # i.e. their default values are opposite and _isSelectionAnchoredAtStart is only updated if the selection has changed. | ||
| self._isSelectionAnchoredAtStart = not self._lastSelectionMovedStart |
There was a problem hiding this comment.
I get that they're reversed, but I don't follow the bit about it only being updated if the selection changes. Would it really matter if isSelectionAnchoredAtStart just always returned the reverse of _lastSelectionMovedStart? I'm thinking we should get rid of _lastSelectionMovedStart and just flip everything to use the new attribute.
There was a problem hiding this comment.
It looks like this was valid before the refactor a while ago, but not anymore.
There was a problem hiding this comment.
On second thought there is a difference. But maybe it is actually the desired behavior.
Flipping around _lastSelectionMovedStart to _isTextSelectionAnchoredAtStart assumes that the end of the seletion is moving unless proven otherwise. This means that if the anchor position is unknown, braille will default to showing the end of the selection.
I'll give it a try and push an update when I'm satisfied.
| review.setCurrentMode('document',True) | ||
| braille.handler.handleGainFocus(self) | ||
|
|
||
| def _get_isSelectionAnchoredAtStart(self): |
There was a problem hiding this comment.
- This should be in DocumentTreeInterceptor, since text is not valid for non-document TreeInterceptors. Only DocumentTreeInterceptors have makeTextInfo.
- Same suggested changes as for this property in NVDAObject.
There was a problem hiding this comment.
- This should be in DocumentTreeInterceptor, since text is not valid for non-document TreeInterceptors.
I believe I put it in the superclass to avoid having to check for that specific subclass when using this attribute. See my previous comment. This would of course not be necessary if TextInfoRegion is always paired with a DocumentTreeInterceptor, but the documentation doesn't make that clear.
|
@dkager commented on 22 Jun 2017, 19:19 GMT+10:
More future proof, but potentially less clear and more error prone. I think i agree here; I'd prefer to see this as an isinstance. Note that you'll need to test for DocumentTreeInterceptor, not just TreeInterceptor. |
|
With regard to I'm not sure I find that clearer. :) |
This is identical to
|
|
@LeonarddeR You mean equivalent. |
| self._readingInfo = readingInfo = sel.copy() | ||
| if self.obj.isSelectionAnchoredAtStart: | ||
| # The end of the range is exclusive, so make it inclusive first. | ||
| readingInfo.move(textInfos.UNIT_CHARACTER, -1, "end") |
There was a problem hiding this comment.
I'm still not happy about this. For a downward multi-line selection it is more practical to see the next line to be selected rather than the very right edge of the last selected line. But there is no good way to detect the difference between the result of pressing shift+downArrow and shift+end. The only solution I know of is to rely on cursor position even when there is a selection, but that doesn't work in all applications.
So there are two options:
- Rely on the cursor where possible. Provides great usability in some programs (Notepad++, Eclipse) and average usability in others (Notepad, Word, browse mode).
- Don't try to be clever and always show the very edge of a selection. This is consistent and predictable, but usability is so-so.
dkager
left a comment
There was a problem hiding this comment.
Addressed comments from James and Leonard and made some small improvements.
| newInfo=self.makeTextInfo(toPosition) | ||
| if oldInfo.isCollapsed: | ||
| self._lastSelectionMovedStart = newInfo.compareEndPoints(oldInfo, "startToStart") < 0 | ||
| self.isTextSelectionAnchoredAtStart = newInfo.compareEndPoints(oldInfo, "startToStart") == 0 |
There was a problem hiding this comment.
@jcsteh Could you please double check this line?
There was a problem hiding this comment.
newInfo is usually a collapsed destination position. Its start isn't yet set to the cursor. For example, if you press control+end, newInfo will be collapsed at the last character in the document at this point in the code. So, I think this should be an exact reversal; i.e. this should be >= 0. Having said that, I can't seem to figure out right now how this would actually break things. It seems to work fine, I guess because it gets set later. Still, I think I'd be more comfortable with an exact reversal, but do run the unit tests to make sure I'm not missing something. :)
| # Starting a new selection, so set the selection direction | ||
| # based on the direction of this movement. | ||
| self._lastSelectionMovedStart = direction < 0 | ||
| self.isTextSelectionAnchoredAtStart = direction > 0 |
There was a problem hiding this comment.
@jcsteh Could you double check this line too?
There was a problem hiding this comment.
This one looks fine, since direction can't be 0 if unit is not specified.
| def scrollToCursorOrSelection(self, region): | ||
| if region.brailleCursorPos is not None: | ||
| self.mainBuffer.scrollTo(region, region.brailleCursorPos) | ||
| elif not isinstance(region, (NVDAObjectRegion, TextInfoRegion)) or not region.obj.isTextSelectionAnchoredAtStart: |
There was a problem hiding this comment.
Motivation for this test:
- If a region is an NVDAObjectRegion it will have an NVDAObject, which has the isTextSelectionAnchoredAtStart attribute.
- If a region is a TextInfoRegion then it has a TextInfo and it will be based on a TreeInterceptor. DocumentTreeInterceptor is the only TreeInterceptor with a TextInfo, so the region must have one of those as its object. This also means the selection anchor attribute is present.
There was a problem hiding this comment.
Unless I'm missing something, I think selection anchoring is only relevant for TextInfoRegion. Even if we're dealing with an NVDAObject (not a DocumentTreeInterceptor), if it has text, it'll also have a TextInfoRegion... and it is this region that we'll be dealing with. So, I think you only need to test for TextInfoRegion.
| def scrollToCursorOrSelection(self, region): | ||
| if region.brailleCursorPos is not None: | ||
| self.mainBuffer.scrollTo(region, region.brailleCursorPos) | ||
| elif not isinstance(region, (NVDAObjectRegion, TextInfoRegion)) or not region.obj.isTextSelectionAnchoredAtStart: |
There was a problem hiding this comment.
Unless I'm missing something, I think selection anchoring is only relevant for TextInfoRegion. Even if we're dealing with an NVDAObject (not a DocumentTreeInterceptor), if it has text, it'll also have a TextInfoRegion... and it is this region that we'll be dealing with. So, I think you only need to test for TextInfoRegion.
| newInfo=self.makeTextInfo(toPosition) | ||
| if oldInfo.isCollapsed: | ||
| self._lastSelectionMovedStart = newInfo.compareEndPoints(oldInfo, "startToStart") < 0 | ||
| self.isTextSelectionAnchoredAtStart = newInfo.compareEndPoints(oldInfo, "startToStart") == 0 |
There was a problem hiding this comment.
newInfo is usually a collapsed destination position. Its start isn't yet set to the cursor. For example, if you press control+end, newInfo will be collapsed at the last character in the document at this point in the code. So, I think this should be an exact reversal; i.e. this should be >= 0. Having said that, I can't seem to figure out right now how this would actually break things. It seems to work fine, I guess because it gets set later. Still, I think I'd be more comfortable with an exact reversal, but do run the unit tests to make sure I'm not missing something. :)
| # Starting a new selection, so set the selection direction | ||
| # based on the direction of this movement. | ||
| self._lastSelectionMovedStart = direction < 0 | ||
| self.isTextSelectionAnchoredAtStart = direction > 0 |
There was a problem hiding this comment.
This one looks fine, since direction can't be 0 if unit is not specified.
| self.hasContentChangedSinceLastSelection=False | ||
| speech.speakSelectionChange(oldInfo,newInfo,generalize=hasContentChanged) | ||
|
|
||
| def _findSelectionAnchor(self,oldInfo,newInfo): |
There was a problem hiding this comment.
There's a slight chance someone might see this method name and expect it to return something, rather than taking action. I think renaming this to _updateSelectionAnchor or similar might make this clearer.
| sel.setEndPoint(readingInfo, "endToEnd") | ||
| else: | ||
| # If there is no selection, use the cursor. | ||
| self._readingInfo = readingInfo = self._getCursor() |
There was a problem hiding this comment.
Sorry; I missed this in my first round of review. If there's no selection, POSITION_SELECTION will be collapsed at the cursor, so this is redundant. That also suggests we should get rid of _getCursor altogether. ReviewTextInfoRegion will need to be updated to just return the review position for _getSelection.
There was a problem hiding this comment.
Does that makes sense to you, semantically speaking? If it does then I'm happy to change it.
Would you discard POSITION_CARET? Intuitively, the following order looks good to me:
- POSITION_SELECTION
- POSITION_CARET
- POSITION_FIRST
Where POSITION_FIRST seems a poor fallback, but better than nothing I guess.
…ere is no selction, _getSelection() returns the cursor.
|
I think all review comments have been resolved. |
|
@dkager commented on 1 Jul 2017, 22:05 GMT+10:
I assume you're referring to the case where there's no selection, only a cursor. It's true that this will call |
jcsteh
left a comment
There was a problem hiding this comment.
Last tiny change. Thanks for your work and your patience. :)
| except: | ||
| # try: | ||
| # return self.obj.makeTextInfo(textInfos.POSITION_CARET) | ||
| # except: |
There was a problem hiding this comment.
Please remove these commented lines. To answer your question, you can discard POSITION_CARET because POSITION_SELECTION should return the caret if there's no selection. Speech relies on this assumption as well.
|
This is now conflicting due to a recent merge to master. Would you mind resolving? Thanks. |
|
Should be fixed. |
Issues have been addressed and superseded by my subsequent review.
This PR fixes #5770.
What
When extending a selection beyond the braille display width, braille keeps showing the unmoving end of the selection. It thus makes it difficult to track what is actually being selected without using speech. This is also true for multi-line selections, where braille will continue to show the top line when you press Shift+downArrow.
How
This patch adds an attribute to NVDAObject to indicate whether the selection is anchored at the start. That is, when the right/bottom end of the selection is moving, the selection is anchored at the start. Otherwise it is not. This attribute is set by CursorManager and EditableText (with and without auto selection detection).
Discussion
There was some discussion about how multi-line selection should be handled. In particular, the question was what should happen when you select entire lines with Shift+downArrow.
The question can be generalized: do you want to see the next character to be selected, or should braille follow the very last character that is still inside the selection?
To avoid having to deal with corner cases, I have chosen the latter approach in this patch. This is easiest to implement, but is it intuitive? Especially when selecting entire lines it may be useful to see the next line you are about to select.
Example
Say you have the text:
When you are on the top line and press Shift+downArrow, braille would show "Hello" with dots 7&8. Another Shift+downArrow shows "World" with dots 7&8, etc.
Testing
Tested so far in Notepad, WordPad, Word 2016, Notepad++, Firefox (browse mode).
CC @LeonarddeR, @tuukkao