Repository: nvaccess/nvda

Issue:
Setting panels do not scroll automatically [Alpha regression]

### Steps to reproduce:

1. Open a setting panel with many items, e.g. Document settings (ctrl+NVDA+D); do not maximize the window.
2. Tab many times until you reach the "Spelling errors" option
3. Tab once again to "Page" option
4.  Continue tabbing

### Actual behavior:

At step 3. we can hear that the option "Page" is focused. But visually, the setting panel did not scroll automatically. Thus, the option is not visible.
At step 4, same for the following options.
Moreover, with Windows Magnifier active, the magnified view continues to follow the invisible focus at the bottom of the setting window and even below. In addition if you activate focus highlight, you will be able to see the position of this invisible focus.

### Expected behavior:
The panel should scroll automatically so that the focus is always visible.

### System configuration
#### NVDA installed/portable/running from source:
From source
#### NVDA version:
Version: source-master-ff79f51
#### Windows version:
Windows 10 1809 (64-bit) build 17763.1817
#### Name and version of other software in use when reproducing the issue:
N/A
#### Other information about your system:
N/A
### Other questions
#### Does the issue still occur after restarting your computer?
Not tested
#### Have you tried any other versions of NVDA? If so, please report their behaviors.
NVDA 2020.4 does not show this issue, i.e. panel scroll automatically with focus move.
#### If add-ons are disabled, is your problem still occurring?
Yes

#### Did you try to run the COM registry fixing tool in NVDA menu / tools?
No



Discussion:
Probably due to recent modifications to GUI (new version of wxPython and follow-up)
Cc @seanbudd

Found this similar issue with a solution posted: https://discuss.wxpython.org/t/tabbing-in-a-scrollpanel/35009, going to investigate
@seanbudd, would you add 2021.1 label for this one since it is a regression in 2021.1 dev cycle.
This issue only impacts sighted and visually impaired people, but since many visually impaired already complain of various NVDA bugs specific to them, it would be wise not to introduce new ones.
Thanks.

Hi @CyrilleB79, it is already added to the milestone - we will fix it in beta if it doesn't make it before then. 
Oh sorry. I had missed it.

Resolving patch:
diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py
--- a/source/gui/nvdaControls.py
+++ b/source/gui/nvdaControls.py
@@ -1,21 +1,20 @@
 # -*- coding: UTF-8 -*-
-#A part of NonVisual Desktop Access (NVDA)
-#Copyright (C) 2016-2018 NV Access Limited, Derek Riemer
-#This file is covered by the GNU General Public License.
-#See the file COPYING for more details.
+# A part of NonVisual Desktop Access (NVDA)
+# Copyright (C) 2016-2021 NV Access Limited, Derek Riemer
+# This file is covered by the GNU General Public License.
+# See the file COPYING for more details.
 
-from ctypes.wintypes import BOOL
-from typing import Any, Tuple, Optional
 import wx
-from comtypes import GUID
+from wx.lib import scrolledpanel
 from wx.lib.mixins import listctrl as listmix
 from .dpiScalingHelper import DpiScalingHelperMixin
 from . import guiHelper
-import oleacc
 import winUser
 import winsound
+
 from collections.abc import Callable
 
+
 class AutoWidthColumnListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
 	"""
 	A list control that allows you to specify a column to resize to take up the remaining width of a wx.ListCtrl.
@@ -355,3 +354,40 @@ def onSliderChar(self, evt):
 			evt.Skip()
 			return
 		self.SetValue(newValue)
+
+
+class TabbableScrolledPanel(scrolledpanel.ScrolledPanel):
+	"""
+	This class was created to ensure a ScrolledPanel scrolls to nested children of the panel when navigating
+	with tabs (#12224). A PR to wxPython implementing this fix can be tracked on
+	https://github.com/wxWidgets/Phoenix/pull/1950
+	"""
+	def GetChildRectRelativeToSelf(self, child: wx.Window) -> wx.Rect:
+		"""
+		window.GetRect returns the size of a window, and its position relative to its parent.
+		When calculating ScrollChildIntoView, the position relative to its parent is not relevant unless the
+		parent is the ScrolledPanel itself. Instead, calculate the position relative to scrolledPanel
+		"""
+		childRectRelativeToScreen = child.GetScreenRect()
+		scrolledPanelScreenPosition = self.GetScreenPosition()
+		return wx.Rect(
+			childRectRelativeToScreen.x - scrolledPanelScreenPosition.x,
+			childRectRelativeToScreen.y - scrolledPanelScreenPosition.y,
+			childRectRelativeToScreen.width,
+			childRectRelativeToScreen.height
+		)
+
+	def ScrollChildIntoView(self, child: wx.Window) -> None:
+		"""
+		Overrides child.GetRect with `GetChildRectRelativeToSelf` before calling
+		`super().ScrollChildIntoView`. `super().ScrollChildIntoView` incorrectly uses child.GetRect to
+		navigate scrolling, which is relative to the parent, where it should instead be relative to this
+		ScrolledPanel.
+		"""
+		oldChildGetRectFunction = child.GetRect
+		child.GetRect = lambda: self.GetChildRectRelativeToSelf(child)
+		try:
+			super().ScrollChildIntoView(child)
+		finally:
+			# ensure child.GetRect is reset properly even if super().ScrollChildIntoView throws an exception
+			child.GetRect = oldChildGetRectFunction
diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py
--- a/source/gui/settingsDialogs.py
+++ b/source/gui/settingsDialogs.py
@@ -14,7 +14,6 @@
 import typing
 import wx
 from vision.providerBase import VisionEnhancementProviderSettings
-from wx.lib import scrolledpanel
 from wx.lib.expando import ExpandoTextCtrl
 import wx.lib.newevent
 import winUser
@@ -483,7 +482,7 @@ def makeSettings(self, settingsSizer):
 		# The provided column header is just a placeholder, as it is hidden due to the wx.LC_NO_HEADER style flag.
 		self.catListCtrl.InsertColumn(0,categoriesLabelText)
 
-		self.container = scrolledpanel.ScrolledPanel(
+		self.container = nvdaControls.TabbableScrolledPanel(
 			parent = self,
 			style = wx.TAB_TRAVERSAL | wx.BORDER_THEME,
 			size=containerDim


Test patch: