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

Allow the review cursor to be bounded to onscreen text #9735

Closed
wants to merge 31 commits into from

Conversation

codeofdusk
Copy link
Contributor

@codeofdusk codeofdusk commented Jun 13, 2019

Link to issue number:

Builds on #9614. Supersedes #9687 and #9899. Closes #9891.

Summary of the issue:

Currently:

  • In the Windows Console, only onscreen text can be read using the review cursor; if text scrolls off the screen, it is impossible to review it.
  • In all other cases, all text in an object is presented to object review, and there is no way to know which text is visible on the screen.
  • In UIA consoles, jumping to the top or bottom lines of the review (shift+numpad 7/9) causes the cursor to become stuck.

Description of how this pull request fixes the issue:

This PR adds new functionality to textInfo objects: an isOffscreen property and POSITION_FIRSTVISIBLE and POSITION_LASTVISIBLE position constants. A new script (bound to NVDA+o by default) toggles review bounds. Review is bounded by default where supported. An implementation of review bounds has been provided for UIA (primarily intended for use in the console) and offsetsTextInfo.

Testing performed:

Attempted to review text before and after running git log in the UIA console using bounded and unbounded modes on Windows 10 1903. Verified that when review is bounded, reviewing the top/bottom lines (shift+numpad 7/9) works as intended.

Known issues with pull request:

  • In UIA consoles, we now diff the entire buffer rather than just the visible portion. This may slightly decrease performance in exchange for improved stability.

Change log entry:

== New Features ==

  • For certain controls (particularly the Windows Console on Windows 10 version 1803 and later), it is now possible to toggle whether the review cursor is bounded by the text currently visible onscreen by pressing NVDA+o. (Allow the review cursor to be bounded to onscreen text #9735)
  • This is especially useful in consoles to read text which has scrolled off the screen.

== Changes for Developers ==

@LeonarddeR
Copy link
Collaborator

Are you aware of how first visible oand last visible offsets are implemented in textInfos.offsets? Given your initial description, I really like this new approach, but it makes sense to implement this for offsets textInfos as well. Note that you can utilize locationHelper to implement an abstract version of isOutOfBounds. It might help you to look at textINfos.offsets.OffsetsTextInfo._get_boundingRects in particular.

@codeofdusk
Copy link
Contributor Author

@LeonarddeR I've added a basic implementation for offsetsTextInfo.

CC @feerrenrut @michaelDCurran

source/NVDAObjects/UIA/__init__.py Outdated Show resolved Hide resolved
source/NVDAObjects/UIA/__init__.py Outdated Show resolved Hide resolved
source/NVDAObjects/UIA/__init__.py Outdated Show resolved Hide resolved
source/NVDAObjects/UIA/__init__.py Outdated Show resolved Hide resolved
source/textInfos/__init__.py Outdated Show resolved Hide resolved
source/NVDAObjects/__init__.py Outdated Show resolved Hide resolved
category=_(u"Text review"),
# Translators: A gesture description.
description=_(u"Toggles whether review is constrained to the currently visible text"))
def script_toggleReviewBounds(self, gesture):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worries me that this is is on the object instead of a global command. Is there a particular reason for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I too think this could be cleaner as a global command, but the reviewBounded variable is also on the object so keeping this there might be better from an organizational standpoint...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question is whether we really believe that review Bounded should be on the object or on the textInfo, or even global. If an object gets destroyed and created again, the state of review Bounded is lost. Having review bounds on tree interceptors also makes sense, i.e. when you want to review with document review what is on screen on a particular page.

I also think that it doesn't make much sense to have review bounds doing anything when in screen review. So in that case, the command should just do nothing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may have been my idea to put this on the object. but @LeonarddeR brings up a good point in that the state will be lost if moving away from the object and back again. The problem with having it global though is that different controls work best when reviewBounded is set in a specific way by default. I.e. consoles probably want it on by default.
Would we be comfortable having bounded on by default globally? I think this could work, but it would be a bit of a change to NvDA's behaviour. Probably a good change though. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, having this as a global default makes sense to me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I also think a global default would be a plus.
It does not make much sense for a "screen" review to easily review what's not on screen.
It can be helpful at times, but is most often confusing when collaborating with a sighted person.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has now been made a global default.

source/globalCommands.py Outdated Show resolved Hide resolved
source/textInfos/offsets.py Outdated Show resolved Hide resolved
source/NVDAObjects/__init__.py Outdated Show resolved Hide resolved
source/globalCommands.py Outdated Show resolved Hide resolved
category=_(u"Text review"),
# Translators: A gesture description.
description=_(u"Toggles whether review is constrained to the currently visible text"))
def script_toggleReviewBounds(self, gesture):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may have been my idea to put this on the object. but @LeonarddeR brings up a good point in that the state will be lost if moving away from the object and back again. The problem with having it global though is that different controls work best when reviewBounded is set in a specific way by default. I.e. consoles probably want it on by default.
Would we be comfortable having bounded on by default globally? I think this could work, but it would be a bit of a change to NvDA's behaviour. Probably a good change though. Thoughts?

description=_(u"Toggles whether review is constrained to the currently visible text"))
def script_toggleReviewBounds(self, gesture):
try:
rp = api.getReviewPosition()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should be moved above the try block. Only isOffscreen is expected to throw NotImplementedError.

)
if outOfBounds:
api.setReviewPosition(
self.makeTextInfo(textInfos.POSITION_CARET), isCaret=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why isCaret=True is necessary here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the textInfo is a caret instance? Maybe it isn't needed...

try:
visiRanges = self.obj.UIATextPattern.GetVisibleRanges()
element = 0 if position == textInfos.POSITION_FIRSTVISIBLE else visiRanges.length - 1
self._rangeObj = visiRanges.GetElement(0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call should use element, not 0 I think.

element = 0 if position == textInfos.POSITION_FIRSTVISIBLE else visiRanges.length - 1
self._rangeObj = visiRanges.GetElement(0)
except COMError:
# Error: FIRST_VISIBLE position not supported by the UIA text pattern.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment should reflect it is first or last position not supported, not just first.

UIAHandler.TextPatternRangeEndpoint_End) >= 0
else:
# Review bounds not available.
return super(UIATextInfo, self).isOutOfBounds()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is supposed to be super's isOffscreen

@@ -702,6 +710,22 @@ def compareEndPoints(self,other,which):
target=UIAHandler.TextPatternRangeEndpoint_End
return self._rangeObj.CompareEndpoints(src,other._rangeObj,target)

def isOffscreen(self):
visiRanges = self.obj.UIATextPattern.GetVisibleRanges()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catch COMError around this call, raising NotImplementedError if so.

@@ -972,8 +929,21 @@ def script_review_activate(self,gesture):
script_review_activate.__doc__=_("Performs the default action on the current navigator object (example: presses it if it is a button).")
script_review_activate.category=SCRCAT_OBJECTNAVIGATION

def _moveWithBoundsChecking(self, info, unit, direction, endPoint=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could probably be moved into the base TextInfo class.
Also, is there a reason why this is non-distructive (I.e. returns a copy)? I don't think you actually ever make use of this. It could just do it distructively and return res like the other move... unless you have a particular plan for this not yet done?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a generalization of the console's old bounds checking code (i.e. we captured an oldRange and didn't update the underlying UIA text range if a move was impossible). Would destructive be okay here?

@michaelDCurran
Copy link
Member

michaelDCurran commented Jun 18, 2019 via email

@michaelDCurran
Copy link
Member

michaelDCurran commented Jun 18, 2019 via email

@codeofdusk
Copy link
Contributor Author

Unless I've missed a call when reviewing, It looks like _moveWithBoundsChecking only ever directly replaces a call to info.move, and always overrides info.

Yes, we can do this, because the original info (rather than a new one) is returned if a move is impossible.

@codeofdusk
Copy link
Contributor Author

@michaelDCurran @LeonarddeR This PR now uses unique IDs for NVDA objects to persist review bounds states when objects are regenerated.

I'd like to move the logic that sets reviewBounded to True for consoles to some sort of initialization method. However, when I try to override the __init__ method on the console, my version never gets called. Is there another way to achieve this?

I'd also like to move script_toggleReviewBounds from NVDAObject to TextContainerObject, but when I try the new script doesn't seem to be bound. I can provide the changes I'm trying to make as a commit or diff. Any ideas of things to look into?

@michaelDCurran
Copy link
Member

NVDAObject overlay classes don't support the standard __init__ constructor. Rather, if you implement an "initOverlayClass" method, this will be called on the instance once the object has been constructed and all overlay classes have been chosen and inserted.

@michaelDCurran
Copy link
Member

Currently TextContainerObject does not inherit from ScriptableObject and therefore scripts are not picked up if placed on this class.
However, it seems that currently all classes that inherit from TextContainerObject also inherit from ScriptableObject directly or indirectly.
Therefore, I suggest making TextContainerObject inherit from ScriptableObject instead of AutoPropertyObject. (Note that ScriptableObject inherits from autoPropertyObject itself).
In documentBase.py: change

class TextContainerObject(AutoPropertyObject):

To

class TextContainerObject(ScriptableObject):

@codeofdusk
Copy link
Contributor Author

Currently TextContainerObject does not inherit from ScriptableObject and therefore scripts are not picked up if placed on this class.
However, it seems that currently all classes that inherit from TextContainerObject also inherit from ScriptableObject directly or indirectly.
Therefore, I suggest making TextContainerObject inherit from ScriptableObject instead of AutoPropertyObject. (Note that ScriptableObject inherits from autoPropertyObject itself).
In documentBase.py: change

class TextContainerObject(AutoPropertyObject):

To

class TextContainerObject(ScriptableObject):

@michaelDCurran Working now, thanks!

@@ -551,6 +553,15 @@ def getMathMl(self, field):
"""
raise NotImplementedError

def _get_isOffscreen(self):
"""
Returns True if this textInfo is positioned outside its object's
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the text is partially visible, what is returned?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on the underlying implementation.

codeofdusk added a commit to codeofdusk/nvda that referenced this pull request Jul 22, 2019
codeofdusk added a commit to codeofdusk/nvda that referenced this pull request Jul 22, 2019
…ls and disable the console bounds checking code when oldRange is also offscreen.
@codeofdusk
Copy link
Contributor Author

Superseded for now by #9957, but I'm willing to look into this again if there's a use case.

@codeofdusk codeofdusk closed this Jul 22, 2019
feerrenrut pushed a commit that referenced this pull request Aug 1, 2019
…e checks (PR #9957)

Builds on #9614
Supersedes #9735 and #9899
Closes #9891

Previously, after the console window was maximized (or the review cursor is otherwise placed outside the visible text), text review is no longer functional.
The review top line and review bottom line scripts do not function as intended.

This commit changes:
- The isOffscreen property has been implemented as UIAUtils.isTextRangeOffscreen.
- When checking if the text range is out of bounds, we now also check that oldRange is in bounds before stopping movement.
- Re-implemented POSITION_FIRST and POSITION_LAST in terms of visible ranges.
codeofdusk added a commit to codeofdusk/nvda that referenced this pull request Sep 25, 2019
…ls and disable the console bounds checking code when oldRange is also offscreen.
codeofdusk added a commit to codeofdusk/nvda that referenced this pull request Sep 25, 2019
codeofdusk added a commit to codeofdusk/nvda that referenced this pull request Nov 20, 2019
michaelDCurran pushed a commit that referenced this pull request Nov 20, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

UI Automation in Windows Console: text review behaves inconsistently when the window is maximized
5 participants