# 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. ```cpp 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 ```xml Specifies LibreOfficeTheme state. ... ``` 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. ```cpp 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. ```cpp 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 `ColorConfig`s (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. ```cpp ... 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. ```cpp 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 ```cpp 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. ```cpp 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. ```cpp void ColorConfig_Impl::Load(const OUString& rScheme) ... if (!ThemeColors::IsAutomaticTheme(sScheme)) { uno::Sequence aSchemes = GetSchemeNames(); bool bFound = std::any_of(aSchemes.begin(), aSchemes.end(), [&sScheme](const OUString& rSchemeName) { return sScheme == rSchemeName; }); if (!bFound) sScheme = AUTOMATIC_COLOR_SCHEME; } ... ```