| # 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 |
| <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. |
| |
| ```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<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; |
| } |
| ... |
| ``` |