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

Support Windows 10/11 Dark Mode #16908

Draft
wants to merge 35 commits into
base: master
Choose a base branch
from

Conversation

TristanBurchett
Copy link
Contributor

@TristanBurchett TristanBurchett commented Jul 25, 2024

Link to issue number:

#16683
This is a p2 issue

Summary of the issue:

Some people with low vision can't read dark text on a light backgroundCurrently,
NVDA doesn't support dark mode, which makes NVDA not usable for these users.

Description of user facing changes

Add support for dark mode in NVDA. This is configurable via "Preferences > Settings > Vision > Color theme".
The default value for this setting is Auto, which matches the system dark mode setting.
Users can also select Light or Dark to force a theme.
In addition, users can choose whether to allow use of undocumented windows APIs,
which is necessary to make the popup context menu dark.

Description of development approach

Added a new "colorTheme" setting in the "general" section. Default value is Auto.
Made this configurable in the Vision panel of the Settings dialog.

Added gui/darkMode.py which contains all the detailed logic. It exposes two functions,
initialize() and handleEvent(). handleEvent() checks if the system is in light or dark mode
and changes the foreground and background color of a particular widget and all of its
children.

Added a FilterEvent function in the toplevel App which automatically calls
handleEvent() whenever a window is created or shown.

Redesigned the Add/Edit Dictionary Entry dialogs, because they used RadioBox
which renders very poorly in dark mode (black text on dark grey background).
I changed them to use Choice dropdown instead.

Ideally this would all be handled by wxPython, but wxPython doesn't support
dark mode (wxWidgets/Phoenix#2413).
The non-python version of wx does have a function MSWEnableDarkMode(),
however even that function has to rely on undocumented APIs. If and when
wxPython supports dark mode properly, it should be fairly easy to rip out this code,
since it isn't very invasive.

Testing strategy:

Manually examined every dialog I could possibly find.

Verified that changing the color theme and then pushing the Apply button
causes the settings dialog color theme to change immediately.

Verified behavior with and without undocumented windows APIs.
Note that changing this setting has no effect until NVDA is restarted.

Known issues with pull request:

  1. MessageBox'es are not themed. An example is the NVDA About dialog.
    These dialogs are extremely modal, and there is no way to gain control
    until after the user dismisses the message box.

  2. Menu bars are not themed. An example can be seen in the Debug Log. Supporting themed
    menu bars would require intercepting several undocumented events and drawing the menu items
    ourselves. An example implementation is described in
    https://github.com/adzm/win32-custom-menubar-aero-theme

  3. Column titles are not themed. An example can be seen in the Dictionary dialogs.
    This is implemented by the wx.ListCtrl class. The C++ implementation of
    wxListCtrl::OnPaint hardcodes penColour, and there is no way to override it.
    See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/listctrl.cpp

  4. Tab controls are not themed. An example can be seen at the top of the Add-In Store.
    This is implemented by the wx.Notebook class. I have not been able to figure out how
    to influence the colors it uses.

  5. BrowseMode ElementListDialog still uses a RadioBox widget, which will render
    poorly in dark mode (black text on dark grey). I did not change this to use a Choice
    instead, because each of the radio buttons has an accelerator key defined, and Choice
    widgets do not support accelerators. Breaking the keyboard accelerators seemed
    worse than making it render properly in dark mode. People who rely on this dialog
    might need to set the NVDA color theme to Light if their system theme is set to dark.

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

Summary by CodeRabbit

  • New Features

    • Introduced customizable color themes: light, dark, and auto, enhancing the application's visual settings.
    • Added a new dark mode feature for improved accessibility and user experience.
    • Implemented a dropdown for selecting entry types in the DictionaryEntryDialog for easier interaction.
  • Bug Fixes

    • Corrected typographical errors in the user guide related to NVDA highlighting.
  • Documentation

    • Enhanced user documentation to cover new features, including color themes and dark mode capabilities.

@hwf1324
Copy link
Contributor

hwf1324 commented Jul 25, 2024

Very great feature.

While I've heard that the native controls used by wxWidget have incomplete support for dark mode, this still makes sense.

Would it be possible to add a config item to configure which mode we are in: auto, light and dark.

@CyrilleB79
Copy link
Collaborator

I have not tested it.

But what about the following windows:

  • python console
  • Add-on Store
  • windows popping up when installing add-ons externally from .nvda-addon in Windows Explorer, e.g. install confirmation window
  • Speech viewer
  • Braille viewer
  • create portable copy
  • COM reg fixing tool
  • NVDA installer

@TristanBurchett
Copy link
Contributor Author

I have not tested it.

But what about the following windows:

  • python console
  • Add-on Store
  • windows popping up when installing add-ons externally from .nvda-addon in Windows Explorer, e.g. install confirmation window
  • Speech viewer
  • Braille viewer
  • create portable copy
  • COM reg fixing tool
  • NVDA installer

Thank you for pointing this out! I have looked into those windows and fixed some of them. I will add the updated code soon.

@AppVeyorBot
Copy link

See test results for failed build of commit 7dbadc3ffc

@TristanBurchett
Copy link
Contributor Author

But what about the following windows:

  • windows popping up when installing add-ons externally from .nvda-addon in Windows Explorer, e.g. install confirmation window
  • create portable copy
  • COM reg fixing tool
  • NVDA installer

Hi @CyrilleB79, my fork of NVDA that I'm editing doesn't have the create portable copy and COM reg fixing tool in the tools menu of NVDA settings. When I install the latest version of NVDA, these dialogs are there. How could I update my local version using the installer?

@CyrilleB79
Copy link
Collaborator

There are instructions to produce NVDA's installer locally, or you can just download the installer from appVeyor artifacts generated by this PR and use it to upgrade your local copy.

Though, if you do not wish to modify your local copy, you may also just tweak NVDA's code for test only, to force these dialogs to appear, modifying some if condition in gui\__init__.py. Eg. replace:
if not globalVars.appArgs.secure and not config.isAppX and not NVDAState.isRunningAsSource():
by:
if True:

@TristanBurchett
Copy link
Contributor Author

Thank you!

@TristanBurchett
Copy link
Contributor Author

Very great feature.

Thanks!

Would it be possible to add a config item to configure which mode we are in: auto, light and dark.

Done!

@AppVeyorBot
Copy link

See test results for failed build of commit 9a40244b77

@hwf1324
Copy link
Contributor

hwf1324 commented Jul 29, 2024

That's great, but I'm having some second thoughts about the placement of the "Colour Theme" setting, maybe it would be better to put it in the "General" category or at the top of the "Visual" category?

There are also windows that are not covered, such as the About dialogue box.
There are also windows for add-ons that need to be considered. I wonder if wxPython has a global way to manage window themes?

There are also problems with the display of controls such as checkbox lists, add-on Store details controls, buttons under the mouse pointer, etc.

There should be a section in the user guide about "colour theme" configuration.

Developer's guide or add-on developer's guide can also be added.

@CyrilleB79
Copy link
Collaborator

As @hwf1324, I wonder if there would be a global way to enable dark mode rather than calling gui.guiHelper.enableDarkMode in the __init__ function of each dialog:

  • is there a wx global parameter/function?
  • if not, would there be a way to add the call in one or a few mother dialog class from which all NVDA dialog inherit? This would allow add-on to honor dark mode without any change, provided they use standard dialogs from NVDA.

But the question of add-ons should be considered in any case.

Also should (and can) dark mode configuration be profile aware?

Re the location of the option, I'd say that the Vision panel is the most suitable, but I have no objection if other people want to put it rather in General or in another panel. The Vision panel is used to configure visual aids, and I think that choosing a color theme can be considered one, even if it is discutable.

It's worth noting that the screen curtain is not a visual aid in my view, and having it in the Vision panel creates confusion: the screen curtain does not help visually impaired people to use the screen / the sight, but it's a privacy feature. Anyway, touching screen curtain is off-topic for this PR.

If you stick with keeping the option in the vision panel, I wonder if it is mandatory that options in this panel be vision providers (see focus highlighter or screen curtain implementation). More specifically I wonder:

  • if there is a technical difficulty to define options that are not vision providers in this panel
  • if there is a design recommandation from NV Access to have only vision providers in this panel

@seanbudd seanbudd added the blocked/needs-product-decision A product decision needs to be made. Decisions about NVDA UX or supported use-cases. label Jul 30, 2024
Copy link
Contributor

coderabbitai bot commented Aug 25, 2024

Walkthrough

The changes introduce a new color theme feature to the NVDA application, allowing users to select between light and dark themes. This includes the implementation of a new configuration option for color themes, enhancements to the user interface for managing settings, and the addition of a dark mode feature. Additionally, various UI elements and settings dialogs have been updated to improve user interaction and accessibility.

Changes

Files Change Summary
source/config/configFlags.py Introduced ColorTheme enumeration with constants AUTO, DARK, and LIGHT, along with a property method for localization.
source/config/configSpec.py Added colorTheme configuration option for theme selection and darkModeCanUseUndocumentedAPIs boolean option.
source/core.py Introduced FilterEvent method in InitLocale for handling UI events related to dark mode.
source/gui/__init__.py Called darkMode.initialize() before creating the main frame to apply dark mode settings.
source/gui/configProfiles.py Replaced wx.RadioBox with wx.Choice for trigger selection in the configuration profile dialog and updated event handling.
source/gui/darkMode.py Implemented dark mode feature, including functions for initializing dark mode and modifying window appearance based on theme settings.
source/gui/settingsDialogs.py Added makeDarkModeSettings and makeVisionProviderSettings methods to improve modularity in the settings dialog.
source/gui/speechDict.py Replaced radio buttons with a dropdown list for selecting entry types in the DictionaryEntryDialog.
user_docs/en/changes.md Documented new features: support for Help Tech Activator Pro displays and light/dark color themes.
user_docs/en/userGuide.md Updated user guide to clarify functionality of highlighting feature and added section on selecting color themes.

Possibly related issues


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Outside diff range, codebase verification and nitpick comments (2)
user_docs/en/userGuide.md (2)

2466-2466: Ensure clarity and adherence to style.

The description for the "Enable Highlighting" checkbox should be clear and follow the one-sentence-per-line style.

Note that checking and unchecking the "Enable Highlighting" checkbox will also change the state of the other checkboxes accordingly. Therefore, if "Enable Highlighting" is off and you check this checkbox, the other checkboxes will also be checked automatically. If you only want to highlight the focus and leave the navigator object and browse mode checkboxes unchecked, the state of the "Enable Highlighting" checkbox will be half-checked.

2483-2490: Ensure clarity and adherence to style.

The new section on color themes should be clear and follow the one-sentence-per-line style.

You can control whether NVDA dialogs use a light background (the "Light" theme) or a dark background (the "Dark" theme). The default theme is "Auto", which causes NVDA to match your operating system's "dark mode" setting. You can try out a different theme by changing the selected theme and then pressing the "Apply" button. This will cause the color of the settings dialog to update immediately.

source/gui/darkMode.py Outdated Show resolved Hide resolved
source/gui/darkMode.py Show resolved Hide resolved
source/core.py Outdated Show resolved Hide resolved
@TristanBurchett
Copy link
Contributor Author

In the past week, I figured out how to change the text color in the add-on store without breaking TextCtrl's elsewhere, and came up with a compromise for CheckListCtrl's that preserves legibility. Here are some updated screenshots:

image
image

Copy link
Contributor

@hwf1324 hwf1324 left a comment

Choose a reason for hiding this comment

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

Is it possible to pull the latest master?

Copy link
Contributor

@hwf1324 hwf1324 left a comment

Choose a reason for hiding this comment

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

In the past week, I figured out how to change the text color in the add-on store without breaking TextCtrl's elsewhere, and came up with a compromise for CheckListCtrl's that preserves legibility. Here are some updated screenshots:

Glad to see this, not sure if there's any point to it yet, but this seems to be the place to set the details style and it seems a bit special.

https://github.com/tristanburchett/nvda/blob/pr/TristanBurchett/16908/source/gui/addonStoreGui/controls/details.py#L167

@hwf1324
Copy link
Contributor

hwf1324 commented Aug 26, 2024

With the disabled CheckListCtrl style, the text is not contrasted enough. Of course this is a non-issue.

图片

It would be much better if the default style of CheckListCtrl could be like this.

图片

In addition to the selected styles

@hwf1324
Copy link
Contributor

hwf1324 commented Aug 26, 2024

One more question I wonder if this will be resolved? It flashes the original style when opening the GUI. I wonder if it's possible to wait for the style to finish changing before showing it.

@SaschaCowley
Copy link
Member

SaschaCowley commented Aug 26, 2024

@TristanBurchett it looks like this is coming along very nicely. Just a couple of questions.

  1. It looks like a couple of controls still aren't fully dark mode styled. This includes scrollbars and checkbox lists. Is there the possibility of styling these?
  2. Where are you getting these colours from? Are they the dark mode colours from Windows, or a palette from elsewhere?
  3. Can you try to find a workaround for the radio button issue? A lot of controls in NVDA and its add-ons may rely on radio buttons, and we can't break such a fundamental control on them.
  4. Can you please change the "Allow use of undocumented windows APIs (unsafe)" checkbox to be an actual WX checkbox? The control you have used there is not fully accessible (it is reported as a button, and its checked/unchecked state is not reported). This would also apply to some of your changes in speech dictionaries, though the changes you have made in those cases should not be necessary once radio buttons are working.
    • Ideally this should also be in advanced settings.

@seanbudd seanbudd added this to the 2025.1 milestone Aug 27, 2024
@SaschaCowley SaschaCowley marked this pull request as draft August 27, 2024 04:01
@seanbudd seanbudd removed this from the 2025.1 milestone Sep 16, 2024
@seanbudd seanbudd added the conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. label Sep 24, 2024
@TristanBurchett
Copy link
Contributor Author

Sorry I've been quiet on this for a while -- school has kept me busy. Anyway, here's a status update.

Can you try to find a workaround for the radio button issue? A lot of controls in NVDA and its add-ons may rely on radio buttons, and we can't break such a fundamental control on them.

Yeah, I understand why you're concerned about potential breakage here.

Unfortunately, styling radio boxes is surprisingly difficult. Here's what I've learned.

  • The most recent version of wxWidgets would probably work with the code I've written here, because it contains this pull request which creates separate widgets for each radio button. However NVDA is using an older version (I think it's 3.2.6), which doesn't have that PR integrated.
  • Since I don't think it's practical for me to try to upgrade the version of wxWidgets used by wxPython and also upgrade the version of wxPython used by NVDA... instead I looked into how to make the old-style RadioBox work. These are what windows calls "static controls".
  • In order to change the foreground and background colors for a static control, windows requires that you handle a special message called WM_CTLCOLORSTATIC. Here's one of many discussions about this. However WM_CTLCOLORSTATIC is not exposed in wxPython.
  • As far as I can tell, the only way to get access to unexposed windows messages is to override the WndProc for the widget in question. wxPython actually has example code for how to do this.
  • When I try to mimic that example (with a trivial wndproc that does nothing but call the previous wndproc), ctypes throws exceptions saying "Exception ignored on converting result of ctypes callback function". I assume I'm just doing something wrong but I haven't figured out what yet.

It looks like a couple of controls still aren't fully dark mode styled. This includes scrollbars and checkbox lists. Is there the possibility of styling these?

If I ever manage to figure out radioboxes then I think checkbox lists will also work, because checkbox lists are also "static controls". Scrollbars are a different matter though -- styling scrollbars requires doing shadowing system DLL symbols on startup. I don't think that kind of low-level trick feels warranted here, or particularly safe.

Where are you getting these colours from? Are they the dark mode colours from Windows, or a palette from elsewhere?

The colors look like the colors that most apps use when dark mode is enabled (e.g. windows settings dialogs, chrome, vscode). Apparently there are no "standard" colors, and every app is expected to do everything on its own. They're easy to change if you want.

Can you please change the "Allow use of undocumented windows APIs (unsafe)" checkbox to be an actual WX checkbox?

I'm confused about this because I'm literally using the same widget type as every other settings dialog checkbox that I could find. In particular, I'm calling wx.CheckBox().

the changes you have made in those cases should not be necessary once radio buttons are working

I went ahead and reverted those changes to the dialogs.

Ideally this should also be in advanced settings.

Sure, no problem. I want to find a way to get unblocked on the radiobox issue first though. Otherwise it sounds like all of this will have been a waste of time?

@SaschaCowley
Copy link
Member

@TristanBurchett thanks for the status update. If you want help with this, please don't hesitate to ask and we'll try our best.

CC @seanbudd

@seanbudd
Copy link
Member

The most recent version of wxWidgets would probably work with the code I've written here, because it contains this pull request which creates separate widgets for each radio button. However NVDA is using an older version (I think it's 3.2.6), which doesn't have that PR integrated.
Since I don't think it's practical for me to try to upgrade the version of wxWidgets used by wxPython and also upgrade the version of wxPython used by NVDA... instead I looked into how to make the old-style RadioBox work. These are what windows calls "static controls".

Are you sure that's right? The PR you linked was merged 2 years ago, and wxPython 4.2.2 is using the latest wxWidgets release. My understanding is that we are now using a wxPython build with that wxWidgets commit, especially since #17181 . There may be implementation issues remaining in wxPython? Have you re-tested this since merging in master? #17181 was merged since the last time you worked on this.

The colors look like the colors that most apps use when dark mode is enabled (e.g. windows settings dialogs, chrome, vscode). Apparently there are no "standard" colors, and every app is expected to do everything on its own. They're easy to change if you want.

I recently did some research on this, I'd like to suggest a more specific colour pallete once this is closer to being ready

@seanbudd seanbudd removed the blocked/needs-product-decision A product decision needs to be made. Decisions about NVDA UX or supported use-cases. label Dec 2, 2024
@seanbudd
Copy link
Member

seanbudd commented Dec 2, 2024

Hey @TristanBurchett - have you managed to test with the latest master merged in? are there any other outlying issues?

@TristanBurchett
Copy link
Contributor Author

@seanbudd Your message gave me hope! So I tested it again after merging master, but unfortunately radio buttons still have issues. I have a couple more ideas to try though:

  • Re-verify my findings from before 17181 was merged in.
  • Use the win32gui.CallWindowProc implementation instead of trying to wrap functions myself with ctypes
  • Give up on the WM_CTLCOLORSTATIC message, and instead try monkeypatching wx.RadioBox

Btw, here is the thread I started with the wxPython people

@seanbudd
Copy link
Member

seanbudd commented Dec 4, 2024

Thanks for linking that thread @TristanBurchett .

Edit: Just checked wx 3.2.6 and it still has the old code, and above code shows no children.

I'm confused by the above comment, does this mean the fix was removed from wxWidgets?
If the fix is in wxWidgets but not wxPython yet, you may need to log a bug with wxPython but I'd first confirm that this is indeed fixed still in wxWidgets.
The commit they reference was merged in 2022, and 3.2.6 is from a much newer wx commit. In fact radiobox.cpp hasn't changed in a year.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants