Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-124490

Allow an application to request a color scheme

    XMLWordPrintable

Details

    • User Story
    • Resolution: Unresolved
    • P2: Important
    • None
    • None
    • GUI: Look'n'Feel
    • None
    • eee575595 (dev), d3b0d414b (dev), bce9d648c (dev), b5215f65c (dev), 25c02ae5b (dev), aae963895 (dev), 8bc80f2e1 (dev), cee94658d (dev), 2c5f5bf2e (dev), 5cdac10b4 (dev)

    Description

      Qt can detect the color scheme of the system and report it via the QStyleHints::colorScheme property.

      However, applications might want to control whether they are dark or light. One option is to set a specific palette, which will however not be respected by all styles (such as the native macOS style), and not on all platforms change the appearance of the non-client area of the window to be consistent with the user-defined palette.

      The suggestion is to add a QStyleHints::setColorScheme API that allows applications to request a certain color scheme. Setting the scheme should

      • make Qt's default palette respect the choice, ideally based on the corresponding system palette
      • adapt the window frame to be consistent with the requested color scheme
      • emit the property's changed signal
      • generate the same events that changing the color scheme using the respective system settings would produce (ThemeChange, ApplicationPaletteChange)

      Not all of this will be possible on all platforms, but since this will be in QStyle*Hints* that should be acceptable. Applications that have very strong requirements to the palette can always set their own.

      Platform Analysis

      The different desktop and mobile platforms provide different mechanisms to let an application choose its appearance.

      Windows ✅

      On Windows, each window has to flag that it supports dark mode by setting the DwmwaUseImmersiveDarkMode or DwmwaUseImmersiveDarkModeBefore20h1 attribute on the window. With this attribute set, the window will get a dark, immersive window frame. On Windows 11, this will give the window a dark frame even if the system is running in light mode.

      In Qt's Windows QPA implementation, we check if the window is using a dark palette (where the text is brighter than the background color):

      static inline bool shouldApplyDarkFrame(const QWindow *w)
      {
      //...
          return windowPal.color(QPalette::WindowText).lightness()
               > windowPal.color(QPalette::Window).lightness();
      }
      

      and set the DwmwaUseImmersiveDarkMode attribute if that is the case.

      This means that applications can use a light palette to opt out of dark mode support. We can always read the light palette by using the old Win32 APIs - this will always produces a standard "light" palette. We do this in QWindowsTheme::populateLightSystemBasePalette. Windows does not provide an API to read the dark palette when the system is not running in dark mode: there is no such thing as a "dark palette", so if an application wants to be dark on a light system, then we need to craft a suitable palette in Qt.

      The default palette of a Qt application also depends on the style: the Windows Vista style will override the palette to be light, because it's pixmap based, reading the assets for the controls using the UXTheme API, which has no concept of dark mode.

      void QWindowsVistaStyle::polish(QPalette &pal)
      {
          Q_D(QWindowsVistaStyle);
      
          if (QGuiApplicationPrivate::colorScheme() == Qt::ColorScheme::Dark) {
              // System runs in dark mode, but the Vista style cannot use a dark palette.
              // Overwrite with the light system palette.
              using QWindowsApplication = QNativeInterface::Private::QWindowsApplication;
              if (auto nativeWindowsApp = dynamic_cast<QWindowsApplication *>(QGuiApplicationPrivate::platformIntegration()))
                  nativeWindowsApp->populateLightSystemPalette(pal);
          }
          // ...
      }
      

      Applications that use the "Fusion" style, however, will automatically use the dark palette if enabled, and cannot (in practice) ask for the "light" system palette (as QWindowsApplication is not a documented class). They have to manually craft a light palette, but in Qt we can read the light palette via QWindowsTheme::populateLightSystemBasePalette.

      macOS ✅

      An application can explicitly select its own appearance to be either dark or light, no matter which appearance the system is running in.

      When changing the appearance at runtime, the system will also generate the palette changes (via NSSystemColorsDidChangeNotification, and Qt will "see" the respective palette as the default palette in qt_mac_createSystemPalette.

      IOW, a QStyleHints::setColorScheme could be fully implemented here.

      iOS ✅

      Appearance can be overridden for each window using the UIView.overrideUserInterfaceStyle property. The documentation states

      If the view is a UIWindow object, the new style applies to everything in the window, including the root view controller and all presented content.

      so we would apply that property to all existing UIWindow instances when the style is explicitly overridden, and also set it when a UIWindow gets created. If the property is not UIUserInterfaceStyleUnspecified, then system appearance changes are ignored, otherwise they will be respected.

      Android ✅

      A dark version of the Material design system is at:

      Android has different concepts for this - dark mode, night mode (which are often assumed to be the same, sometimes not), plus themes. Support for a dark theme was implemented in https://codereview.qt-project.org/q/change:Id26059231f41761d822d494ac6c641bf3cba3322

      Several different versions of APIs exist to set an appearance on an activity level, but they are all problematic. Changing the configuration of an Activity's Resources has no effect (perhaps unless the Activity is recreated). Also, the updateConfiguration API is deprecated (and apparently not present on more recent images, at least I see JNI warnings). The replacement API is documented to be Context::createConfigurationContext, but that creates a new context, which we might not be able to do.

      The established way of doing this is apparently to use androidx.appcompat.app.AppCompatDelegate, which has setDefault/LocalNightMode. However, this requires that we use AppCompat versions of classes and APIs throughout our Android implementation.

      It is unlikely that we can implement this feature through system APIs, but using the logic added when dark theme support was introduced, we can fake things ourselves through a Qt specific override.

      Linux Desktop ⛔️

      TBD

      Attachments

        Issue Links

          For Gerrit Dashboard: QTBUG-124490
          # Subject Branch Project Status CR V

          Activity

            People

              vhilshei Volker Hilsheimer
              vhilshei Volker Hilsheimer
              Maycon Stamboroski Maycon Stamboroski
              Volker Hilsheimer Volker Hilsheimer
              Votes:
              1 Vote for this issue
              Watchers:
              5 Start watching this issue

              Dates

                Created:
                Updated: