Magnifier integration #19172
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR adds a comprehensive NVDA Magnifier feature with three magnification modes (fullscreen, docked, and lens) along with extensive unit test coverage. The implementation includes color filtering, zoom controls, spotlight mode, and various focusing mechanisms.
- Implements NVDAMagnifier classes for fullscreen, docked, and lens magnification with Windows API integration
- Adds keyboard shortcuts and scripts in globalCommands.py to control the magnifier
- Provides comprehensive unit test coverage for all magnifier components
Reviewed Changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
| source/NVDAMagnifier/init.py | Core magnifier implementation including base class, fullscreen, docked, lens magnifiers, and spotlight manager |
| source/NVDAMagnifier/windowsHandler.py | Window management for different magnifier types with panel rendering and color filter application |
| source/globalCommands.py | Global command scripts and keyboard shortcuts for controlling the magnifier |
| tests/unit/test_NVDAMagnifier/test_window.py | Tests for window handling components (panels and frames) |
| tests/unit/test_NVDAMagnifier/test_fullscreenMagnifier.py | Tests for fullscreen magnifier functionality |
| tests/unit/test_NVDAMagnifier/test_dockedMagnifier.py | Tests for docked magnifier functionality |
| tests/unit/test_NVDAMagnifier/test_lensMagnifier.py | Tests for lens magnifier functionality |
| tests/unit/test_NVDAMagnifier/test_spotlightManager.py | Tests for spotlight animation manager |
| tests/unit/test_NVDAMagnifier/test_NVDAMagnifier.py | Tests for base magnifier class functionality |
Comments suppressed due to low confidence (1)
source/NVDAMagnifier/windowsHandler.py:168
- This comment appears to contain commented-out code.
# if self.colorFilter == "normal":
# return image
# elif self.colorFilter == "greyscale":
# for y in range(height):
# for x in range(width):
# r, g, b = image.GetRed(x, y), image.GetGreen(x, y), image.GetBlue(x, y)
# gray = int(0.299 * r + 0.587 * g + 0.114 * b)
# gray = max(0, min(255, gray))
# image.SetRGB(x, y, gray, gray, gray)
# elif self.colorFilter == "inverted":
# for y in range(height):
# for x in range(width):
# r, g, b = image.GetRed(x, y), image.GetGreen(x, y), image.GetBlue(x, y)
# image.SetRGB(x, y, 255 - r, 255 - g, 255 - b)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def startMagnifying(self, colorFilter: str) -> None: | ||
| """Start the magnification with lens-specific settings.""" | ||
| self.setColorFilter(colorFilter) |
There was a problem hiding this comment.
This method requires 2 positional arguments, whereas overridden GlobalFrame.startMagnifying requires 1.
| def startMagnifying(self, colorFilter: str) -> None: | |
| """Start the magnification with lens-specific settings.""" | |
| self.setColorFilter(colorFilter) | |
| def startMagnifying(self) -> None: | |
| """Start the magnification with lens-specific settings.""" | |
| # Assumes self.colorFilter is set before this is called | |
| self.setColorFilter(self.colorFilter) |
| self.setColorFilter(colorFilter) | ||
| super().startMagnifying() | ||
|
|
||
| def updateMagnifier(self, focusX: int, focusY: int, zoomLevel: float, colorFilter: str) -> None: |
There was a problem hiding this comment.
This method requires 5 positional arguments, whereas overridden GlobalFrame.updateMagnifier requires 4.
| @@ -0,0 +1,843 @@ | |||
| import ctypes | |||
There was a problem hiding this comment.
Module 'ctypes' is imported with both 'import' and 'import from'.
| @@ -0,0 +1,241 @@ | |||
| import NVDAMagnifier | |||
There was a problem hiding this comment.
Module 'NVDAMagnifier' is imported with both 'import' and 'import from'.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
seanbudd
left a comment
There was a problem hiding this comment.
Thanks @Boumtchack - this is in a really solid state feature and design-wise.
I've given some broader comments on code layout and styling issues.
The test coverage here is really great.
| Args: | ||
| parent: Parent window | ||
| panelType: Either "docked" or "lens" to determine behavior |
There was a problem hiding this comment.
Please use our docstring style, sphinx
| Args: | |
| parent: Parent window | |
| panelType: Either "docked" or "lens" to determine behavior | |
| :param parent: Parent window | |
| :param panelType: Either "docked" or "lens" to determine behavior |
| import wx | ||
|
|
||
|
|
||
| class GlobalPanel(wx.Panel): |
There was a problem hiding this comment.
lets go with "MagnifierBase" rather than "Global" when talking about code that serves as a shared base for other code. Global tends to imply functionality that is enforced globally, not where it can be overwritten with inheritance
| class GlobalPanel(wx.Panel): | |
| class MagnifierBasePanel(wx.Panel): |
| @@ -0,0 +1,842 @@ | |||
| import ctypes | |||
There was a problem hiding this comment.
Please add our copyright headers: https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/copyrightHeaders.md
| @@ -0,0 +1,842 @@ | |||
| import ctypes | |||
There was a problem hiding this comment.
please refactor the majority of this code into files within this folder. The init file should really only have high level logic such as initialization and termination of the module.
| @@ -0,0 +1,842 @@ | |||
| import ctypes | |||
There was a problem hiding this comment.
Please rename this submodule to just "magnifier"
| self.drawPlaceholder(dc) | ||
|
|
||
|
|
||
| class GlobalFrame(wx.Frame): |
There was a problem hiding this comment.
I would suggest moving the frames to their own submodules magnifier.frames.base, magnifier.frames.docked, etc
| ) | ||
|
|
||
|
|
||
| # Base Code |
There was a problem hiding this comment.
these kinds of comments can largely be removed. code grouping should be largely done through separate files
| category=SCRCAT_VISION, | ||
| gestures=["kb:windows+numLock+numpadPlus", "kb:windows+numpadPlus", "kb:windows+="], | ||
| ) | ||
| def script_startMagnifier(self, gesture): |
There was a problem hiding this comment.
please add type hints for gesture params
| category=SCRCAT_VISION, | ||
| gestures=["kb:windows+numLock+numpadPlus", "kb:windows+numpadPlus", "kb:windows+="], | ||
| ) | ||
| def script_startMagnifier(self, gesture): |
There was a problem hiding this comment.
Would you be able to define the function logic for these commands in magnifier.commands, so that globalCommands.script_startMagnifier just calls a function in magnifier.commands?
|
|
||
| super().__init__( | ||
| frameType="lens", | ||
| title="NVDA Lens Magnifier", |
There was a problem hiding this comment.
make sure strings like this are translated
There was a problem hiding this comment.
| title="NVDA Lens Magnifier", | |
| # Translators: ... | |
| title=pgetttext("magnifier", "Lens Magnifier"), |
| @@ -0,0 +1,347 @@ | |||
| import array | |||
There was a problem hiding this comment.
Could you please also add a changelog entry and user guide entry?
There was a problem hiding this comment.
I'm not sure what you are referencing. what should that look like?
There was a problem hiding this comment.
NVDA has a changelog to list changes in each release of NVDA:
- in source at
user_docs\en\changes.md - hosted at https://download.nvaccess.org/releases/stable/documentation/changes.html
- docs on how to contribute it: https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/contributing.md#change-log-entry
We also expect general user documentation in the user guide:
- in source at
user_docs\en\userGuide.md - hosted at https://download.nvaccess.org/releases/stable/documentation/userGuide.html
- docs on how to contribute it: https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/userGuideStandards.md
Please take your best pass at it, and we can fix up any issues with English, and make it more user friendly
|
@Boumtchack - I've updated the PR description (CoPilot assisted). Can you please review it and update as appropriate? |
| category=SCRCAT_VISION, | ||
| gesture="kb:control+alt+m", | ||
| ) | ||
| def script_cycleMagnifierMode(self, gesture): |
There was a problem hiding this comment.
Can you also add a new settings panel to retain some of these settings. This is so people don't have to change magnifier modes every time they start. Please create a new settings panel for the magnifier, and add a setting for current magnifier mode.
Extra note: An unbound (e.g. no default gesture) script to open the setting panel is required for each new settings panel.
| category=SCRCAT_VISION, | ||
| gesture="kb:NVDA+shift+q", | ||
| ) | ||
| def script_toggleFullscreenMode(self, gesture): |
There was a problem hiding this comment.
same here in regards to a setting in a settings panel
| category=SCRCAT_VISION, | ||
| gesture="kb:control+alt+i", | ||
| ) | ||
| def script_cycleColorFilter(self, gesture): |
There was a problem hiding this comment.
same here in regards to a setting in a settings panel
| category=SCRCAT_VISION, | ||
| gesture="kb:windows+escape", | ||
| ) | ||
| def script_StopMagnifier(self, gesture): |
There was a problem hiding this comment.
lets remove this and just use a single script_toggleMagnifier rather than script_startMagnifier or script_StopMagnifier. That way a single gesture turns the magnifier on or off. This should also be a setting, so magnifier starts by default with NVDA starts if desired.
| category=SCRCAT_VISION, | ||
| gestures=["kb:windows+numLock+numpadMinus", "kb:windows+numpadMinus", "kb:windows+$"], | ||
| ) | ||
| def script_zoomOut(self, gesture): |
There was a problem hiding this comment.
Also a setting for default zoom level
Of course! |
|
I have not done thorough tests nor a deep review. However here is already my first feedback, both for @Boumtchack but more for NV Access (@seanbudd).
I'd recommend to make a complete list of NVDA Magnifier commands, present and probable future ones, and to check its consistency.
Note: FYI, I am the author of Windows Magnifier add-on (available in NVDA Add-on Store). It's not at all another implementation of a magnifier for NVDA. It's an NVDA add-on meant to work when native Windows Magnifier is being used and that provides two things:
|
|
Hi @CyrilleB79 - thanks for your thorough feedback To answer some of your questions (1) - We've been working closely with a few community members to get this up. We have a rough design plan - I think we have just failed to publish it publicly.
I wonder if we can pass through the keys if NVDA magnifier is disabled somewhere. Otherwise I think using windows magnifier keys as a rough base is useful for portability, and I don't think there's anything wrong with tweaking them slightly to optimise for our more typical patterns, e.g. toggles. I agree that comparing commands more formally is a good idea. |
|
I'll look into creating a GitHub discussion which will include our design plans and roadmap |
|
Thanks @seanbudd for your answers. Below my reactions:
If we get a working and user-friendly NVDA magnifier in 2026.1, I doubt someone will make so many efforts to convert it to a VEP later, unless there is a strong compelling reason identified to convert it in the future. The drawback is that NVDA's code will be less consistent. On the other side, being personally a magnifier user, I'd really push to have a first version of NVDA magnifier available as soon as possible, no matter if it's a VEP or not; I do not wish to wait two more years just because we have not been able to make NVDA magnifier a VEP.
Why? I can imagine use cases where profile specific magnifier configurations could be useful.
Is there a documented GitHub issue for this? I have never encountered this, nor heard of this.
Wouldn't it be the opportunity to improve the documentation instead? Well, we end up with:
I wonder if VEPs are useful or not. They are quite complex; I have tried to implement one once, without success, maybe by lack of documentation? If it does not interest NV Access nor add-on developers, is it worth keeping this complexity? The fact that all VEPs end up in the Vision panel and that, by default, there is a strong link between the GUI and the VEP's configuration do not make them easy nor appealing to implement.
I imagine it's quite easy if you keep the same gesturs as Windows Magnifier: check if NVDA magnifier is running and if not, execute
OK. I have no very strong opinion on this for the moment. |
|
@CyrilleB79 the idea of something like Vision Enhancement Providers is to modularize code. |
OK, no problem. Clearly you should do what seems more efficient to you.
For now, the choice seems to be to make NVDA Magnifier settings global settings, i.e. not profile-dependent. |
|
Hello everyone, I've red all your comments and I'll start doing the changes, and at the same time, I was going to work on the step-by-step submission like you asked if it's still something you'll like me to do.
You also talked a little bit about VEP and I'm not sure about what it means for me. Regarding design I tried to copy what I had in windows magnifier, I think the main parts are for the non-fullscreen magnifiers, as they requier frames and panel so we can look at this when everything before is working fine maybe? I'll follow your lead on the design. Also for color filters, I just did the basic ones, I think I remember when we talk with Julien that color inversion is not possible atm with how Windows work, but we can look into wich color filter can be added like for color blindness Also I can change the commands gesture, but I'm not sure I'm the best to choose what these would be, so just tell me what is better for you, I think you know a lot more gestures than I do. if you can give me a list of the gesture you wants for:
I'm not sure how to work on that if you could give me an exemple also.
I think it would be nice to know wich one we would implement in the future so I can prepare the code accordingly even if it's not top priority.
I think I can already look into this as a main feature. |
|
Thanks for your answers. Below my first reactions:
VEP stands here, unofficially, for vision enhancement provider (see
Again, I'll let NV Access (Sean) provide official guidelines.
I have experimented too with color transformation (see this unfinished piece of code). I can confirm that Windows color transformation matrix does not allow to perform all possibl color transformation that one can find in other magnification software such as Zoomtext, e.g. invert brightness). IMO, there is no problem starting with few color filters and adding more of them if / when needed. You may implement standard Windows color filter that probably fit the most common use cases. |
I guess that Sean's point is:
Sean, please confirm.
OK. I'll try to think to something consistent. Else, let's stick with something similar as Windows Magnifier, following Sean's reasoning. In any case, this can be changed after the merge if needed.
The settings panels are in the dialog tha can be opened through NVDA menu -> Preferences -> Settings... You have one panel per category and all these panels are implemented in gui/settingsDialogs.py
I can imagine so many features that would be useful in a magnification software. A software such as Zoomtext for example has much more options and commands than Windows Magnifier and is highly customizable. Below are some examples CommandsCommands to move the magnified view
Comands to move to view
Commands to control settingsThese commands are shortcuts that can control one or more setting without having to open the settings dialog. NVDA is full of such commands. These commands do not need to have an assigned gesture. Only users who need it will assign them.
Notes:I have put between brackets:
Parameter (setting)
|
|
I'll be looking at that next monday, |
|
Thanks @CyrilleB79 for the assistance - I think you covered most of how I would answer @Boumtchack's questions. |
|
Hello guys, So I've started a new branch with only the fullscreen magnifier, no border and no relative mode, no color filter. I've taken into account some of the suggestions, but I'm still don't understand on how the setting pannel should be done. I'm just gonna make sure everything is working fine this morning and I'm pushing it asap. So maybe we can make the settings together? |
|
Hi @Boumtchack - should we close this PR or do you intend to work on it separately? I've DMd you on slack in regards to getting settings panels together, unless you've contact Cyrille off-GitHub already? |
|
I have written to @Boumtchack by e-mail. To summarize:
|
|
Regarding my new PR, it was talked about that we do step by step implementation of the magnifier to work on little bit of what I had done so far. I'll let @seanbudd decide whether or not I'm keeping this pr or the new one. This one was primarelly to show what work I've already have. |
|
OK. No problem. Do as you wish. Feel free to contact me if/when you need and I'll let you know my availability. |
|
@CyrilleB79 - yes, my initial comment was encouraging work to continue at the scope of this PR. |
|
Closing in favour of #19228 |
Link to issue number:
Closes #12539
Summary of the issue:
This pull request introduces a new NVDA magnifier feature, including docked and lens magnifier modes, color filter support, and a set of global commands for controlling magnification. The changes add new classes for handling magnifier windows, provide optimized color filtering, and implement keyboard shortcuts for toggling magnification, zooming, cycling modes, and color filters.
Description of user facing changes:
Implemented new script commands in
globalCommands.pyfor starting/stopping the magnifier, zooming in/out, cycling color filters, toggling fullscreen mode, cycling magnifier types, and spotlighting the magnifier window, each with descriptive messages and gestures.Description of developer facing changes:
TODO
Description of development approach:
Added new classes (
GlobalPanel,GlobalFrame,DockedFrame,LensFrame) inwindowsHandler.pyto implement docked and lens magnifier windows, with crosshair overlays, color filter support, and optimized screen capture and scaling.Testing strategy:
unit tests:
system tests:
manual tests
tests/manualwhich goes over how to manually test the magnifierKnown issues with pull request:
docked window can't be resized, lens magnifier is not centered on the mouse due to zooming infinitly
Code Review Checklist: