Files
core/vcl/README.themes.md
Andrea Gelmini 3bf06ce8df Fix typos
Change-Id: Ia38dd2e506e2fbbaa778c717de92ef7c1e3d108f
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/183060
Tested-by: Julien Nabet <serval2412@yahoo.fr>
Reviewed-by: Julien Nabet <serval2412@yahoo.fr>
2025-03-19 11:17:43 +01:00

10 KiB

LibreOffice Themes

How to read this

It is suggested that you have the code open side-by-side and first read some part here and then the code that it talks about.

VLC Plugins and the UpdateSettings functions

LibreOffice VCL (a bunch of interfaces and a base implementation) has four main platform specific implementations (gtk, qt, win, osx). Each VCL plugin has an UpdateSettings(AllSettings& rSettings) function somewhere. This function reads styling information like colors from the platform specific widget toolkit and loads the information into the StyleSettings instance passed in the argument (indirectly through AllSettings).

The StyleSettings Instance

The StyleSettings (SS) class manages the colors. Various parts of the codebase call getters and setters on SS to get the default color, or to override it. There exists a static StyleSettings instance in the application, and the instances which are created here and there are merged with that static SS instance. we can access the static instance from anywhere in the application by the following function call.

const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings();

How UserPreferences are Saved (registry)

There exist two kind of files for state/configuration management, .xcu and .xcs files. These are XML files, the .xcs files are XML schema files which define the "schema" for the configuration like a colorscheme node will have the following entries colors , and the colors will have a light and a dark variant... The .xcu files are the configuration data files which define the default values for the configuration nodes defined in the schema files.

We use the term registry to refer to the application's configuration and we save the modifications to the default values (set in the .xcu files) in a file named registrymodifications.xcu which lives in $XDG_CONFIG_HOME/libreoffice/(somewhere here).

ColorConfig, ColorConfig_Impl, and EditableColorConfig

From the themes/colors perspective, think of ColorConfig_Impl as a code representation of the colors in the registry, and think of ColorConfig as a *read-only wrapper over ColorConfig_Impl. There exists another class in this bunch named EditableColorConfig, and as the name suggests it is a read-write wrapper over ColorConfig_Impl.

The "Appearance" tab on the "Options" dialog interacts with the registry thanks to an EditableColorConfig instance.

Getting System Colors into the static StyleSettings object

So if you setup some printfdebugging statements in the UpdateSettings functions and in the ColorConfig constructor, you will find that when the application starts, first the UpdateSettings functions are executed, and then the first every ColorConfig instance is created.

Also if you add and set a non-static flag to the StyleSettings and print it out from the UpdateSettings functions and the ColorConfig constructor, you will find that the flag doesn't make it to the static instance (accessed from in ColorConfig) immediately. We use such a flag mbSystemColorsLoaded to see if the static StyleSettings object has the system colors or not.

The LibreOfficeTheme registry flag

<prop oor:name="LibreOfficeTheme" oor:type="xs:short" oor:nillable="false">
  <info>
    <desc>Specifies LibreOfficeTheme state.</desc>
    <label>LibreOffice Theme</label>
    ...

To enable or disable theming, we have a LibreOfficeTheme enum in the registry which is represented by enum class ThemeState. in the code. The default value is ENABLED and the only way for the user to disable it is by changing it to 0 in the expert configuration.

It's still a dispute whether to enable or disable a theming by default, so please refer to the .xcs file and don't take the explanation for implementation.

enum class ThemeState
{
    DISABLED = 0,
    ENABLED = 1,
    RESET = 2,
};

High Level Code overview of Themes Implementation

We load the colors from the widget toolkit into the StyleSettings object and set a flag mbSystemColorsLoaded to true. Then in the ColorConfig constructor ColorConfig::SetupTheme(). We will be back to SetupTheme after we understand how theme colors are stored.

Talking about Singleton ThemeColors class and the path Colors travel

themecolors.hxx defines a singleton class named ThemeColors. This class has two static members. The second one is that of the class itself, and the first one is a boolean flag which is used to check if theme colors are cached or not.

class VCL_DLLPUBLIC ThemeColors
{
    ThemeColors() {}
    static bool m_bIsThemeCached;
    static ThemeColors m_aThemeColors;
    ...

All the colors are essentially registry values grouped in colorschemes and accessed using various ColorConfigs (ColorConfig_Impl, EditableColorConfig, ColorConfig), we just talked about it above. So the theme colors (colors for the UI) are loaded from the registry into this singleton ThemeColors instance, and we set the m_bIsThemeCached flag to true. Then the various VCL plugins check the flag and if the theme colors are cached, these colors are sent to the widget toolkit in different ways depending on the toolkit, like css in case of gtk, QPalette in case of Qt.

Then when the UpdateSettings function is called again, the colors read from the widget toolkit are these custom colors. Then the StyleSettings object is loaded with these colors and they make it to every corner of the application which gets its colors from StyleSettings object.

Back to ColorConfig::SetupTheme()

So in ColorConfig::SetupTheme(), we first check if LibreOfficeTheme enum is set to DISABLED, and if so then we mark ThemeColors as not cached, so no custom colors are set at the toolkit level and return from the SetupTheme() function. Then we check if LibreOfficeTheme is set to RESET which happens when the user presses the Reset All button (after which he restarts the system). If true then we check for mbSystemColorsLoaded to see if the default colors from the widget toolkit have made it to the static StyleSettings instance or not, and if that's true as well, we set LibreOfficeTheme enum to ENABLED

Then in the last part of SetupTheme(), which we reach only if LibreOfficeTheme is set to ENABLED, we check if the theme colors are cached or not (if the UI colors are loaded from the registry into the static ThemeColors instance or not). If cached, we don't touch those over and over. If theme colors are not cached, then we Load the CurrentScheme which means that we load the colors for the current scheme from the registry and store them in ColorConfig_Impl instance.

    ...
    if (!ThemeColors::IsThemeCached())
    {
        // registry to ColorConfig::m_pImpl
        m_pImpl->Load(GetCurrentSchemeName());
        m_pImpl->CommitCurrentSchemeName();

        // ColorConfig::m_pImpl to static ThemeColors::m_aThemeColors
        LoadThemeColorsFromRegistry();
    }
    ...

Then the LoadThemeColorsFromRegistry function is called which loads colors from the registry into the ThemeColors instance by calling ColorConfig::GetColorValue for each entry. In ColorConfig::GetColorValue call, if the color value in the registry is COL_AUTO then we call ColorConfig::GetDefaultColor which returns hardcoded default colors for the document, and StyleSettings colors for the UI (see lcl_GetDefaultUIColor).

If the color value is not COL_AUTO, then the value from the registry is returned, this way we save the user's preferences and get the default colors from hardcoded colors array and StyleSettings.

void ColorConfig::LoadThemeColorsFromRegistry()
{
    ThemeColors& rThemeColors = ThemeColors::GetThemeColors();

    rThemeColors.SetWindowColor(GetColorValue(svtools::WINDOWCOLOR).nColor);
    rThemeColors.SetWindowTextColor(GetColorValue(svtools::WINDOWTEXTCOLOR).nColor);
    ...

What happens when "Reset All" is pressed

When the Reset All button is pressed, all the registry color values are set to COL_AUTO and LibreOfficeTheme is set to RESET. Then after restart, the IsThemeReset conditional in ColorConfig::SetupTheme() checks if StyleSettings has the system colors or not, and once it has, LibreOfficeTheme is set to ENABLED which then goes through the last conditional and LoadThemeColorsFromRegistry is called (just explained above). Since all the registry entries were set to COL_AUTO, we end up getting default values for all the colors (hardcoded ones for document and StyleSettings colors for UI).

ColorConfigValue now has nLightColor and nDarkColor entries

struct ColorConfigValue
{
    bool        bIsVisible; // validity depends on the element type
    ::Color     nColor; // used as a cache for the current color
    Color       nLightColor;
    Color       nDarkColor;
    ...

Each color entry has two color values, one for light and one for dark. Based on the ApplicationAppearance, either light or dark color value is used. Since the nColor "variable name" is used in 250+ places in the codebase, I found it unreasonable to replace all the 250+ references with a conditional like this.

Color nColor;
if (IsDarkMode())
    nColor = aColorConfig.GetColorValue( svtools::APPBACKGROUND ).nDarkColor;
else
    nColor = aColorConfig.GetColorValue( svtools::APPBACKGROUND ).nLightColor;

This would have been very inefficient because IsDarkMode() is a virtual function (being called 250+ times, maybe every frame??). So instead of using a conditional, I use nColor as the cache. When the colors are loaded from the registry (see ColorConfig_Impl::Load), I cache the value into nColor based on ApplicationAppearance value (whether light or dark mode). And since we ask the user to restart the application after changing appearance or color values, caching works without any issues.

Automatic scheme as the fallback

In case the scheme that you are trying to load doesn't exist because "the extension was removed?", or "someone edited the registry".. the "Automatic" scheme is used as the fallback.

void ColorConfig_Impl::Load(const OUString& rScheme)
    ...
    if (!ThemeColors::IsAutomaticTheme(sScheme))
    {
        uno::Sequence<OUString> aSchemes = GetSchemeNames();
        bool bFound = std::any_of(aSchemes.begin(), aSchemes.end(),
            [&sScheme](const OUString& rSchemeName) { return sScheme == rSchemeName; });

        if (!bFound)
            sScheme = AUTOMATIC_COLOR_SCHEME;
    }
    ...