Skip to content

Adding a new component

Sam edited this page May 29, 2023 · 41 revisions

Introduction

When should I use components?

Components in Auto Dark Mode are usually sufficient to implement most features related to theme switching. For all functionality that is related to switching a theme from light to dark, or vice versa, a component is used. If you find yourself limited by that, please go to the "Adding a new module" section after careful consideration. Components get called on every theme switch and do not get registered to a timer, but to the component manager.

How do they function?

Auto Dark Mode runs modules on a Timer, which in turn call a switch theme instruction. This instruction checks all registered components by calling ComponentNeedsUpdate to see whether a component needs to run code. If this method returns true, Switch is called automatically for those components. And this invokes HandleSwitch, which is implemented by you!

Since the timer is called periodically, it is important that ComponentNeedsUpdate returns false when no change in configuration or state occurred since the last update.

A convention we have adhered to currently is to use a variable (or similar) in each component that tracks if it is currently in dark or light mode. If a user changes a component setting, this variable needs to either be updated, or set to Unknown

Example for a simple component:

  • User changed a setting in a component, but the theme is still the same. However, the component needs to run to put itself back into a known state: ComponentNeedsUpdate returns true
  • User switched from light to dark mode, and component is currently still in state light: ComponentNeedsUpdate returns true
  • User switched from light to light (timer or maybe different setting was changed), but no changes are made to the component configuration: ComponentNeedsUpdate returns false
  • All other similar cases to the one above: ComponentNeedsUpdate returns false

Components

How do I implement a new component?

Generally, a component consists of two things: A component class, and a settings class. Each component class inherits from a Base class called BaseComponent<T>, where T denominates the Class of the Settings object.

They must be placed in AutoDarkModeSvc\SwichComponents\Addons

Minimum working setup

 class MyComponentSwitch : BaseComponent<object> 
 {
    // if you need a constructor, call it like this
    public MyComponentSwitch() : base() { /*your code here*/ }
    public override bool ThemeHandlerCompatibility => false;

    // see below for more detailed description
    protected override bool ComponentNeedsUpdate(SwitchEventArgs e)
    {
        // your code here
    }
    
    protected override void HandleSwitch(SwitchEventArgs e)
    {
        // your code here
    }
 }

Exception handling

You are expected to handle all exceptions in the component.

Overrides

The three main methods/properties you need to implement to get a component to work are:

Method or Property Purpose
ThemeHandlerCompatibility true or false depending on whether you want your component to be compatible with full windows theme switching.
I.e, does it add something to the native theme switching, or does it mirror a feature? For the former, set to true, for the latter, set to no.
Example: Color filters and office switch are compatible, but not setting the apps theme
ComponentNeedsUpdate This is the place to set the component update logic as mentioned before
HandleSwitch The method that is called when a theme switch is invoked automatically or by a user. This is only called if ComponentNeedsUpdate returned true. You may not call this method on your own, unless you have a really solid explanation as to why you need this.

The following properties and methods are available to you in the base class:

Method or Property Purpose
ForceSwitch Whether the component must update. Note: This property will return back to false once a theme switch was invoked once with it
Settings A settings object containing the component base settings, as well as your custom settings object
SettingsBefore the same settings object, but one revision older. In case you need to detect changes in configuration in the component to set the state

The following methods and properties are available to you to override in case you need extra functionality

    // the order in which the component is invoked when switching to light or dark mode
    public override int PriorityToLight => 30;
    public override int PriorityToDark => 30;

    // called whenever the component is enabled for the first time (Settings.Enabled changes to true).
    protected override void EnableHook()
    {
        // your code here
    }

    // called whenever the component is disabled (Settings.Enabled changes to false).
    // The disable hook must set all internally tracked settings to a neutral state
    // as components classes are never dereferenced.
    protected override void DisableHook()
    {
        // your code here
    }

    // see below for more detailed description
    protected override void UpdateSettingsState(object newSettings)
    {
        // your code here
    }

    // call this when you need logic performed after Auto Dark Mode switched the theme instead of before
    protected override void Callback(Theme newTheme, SwitchEventArgs e) 
    {
        // your code here
    }

    // set this to true if your component will cause DWM to refresh itself
    public override bool TriggersDwmrefresh => false;

    // set this to true if your component requires a DWM refresh
    public override bool NeedsDwmRefresh => false;

How to use UpdateSettingsState

It is called whenever the AutoDarkMode Config file is updated and BEFORE ComponentNeedsUpdate and Switch.

Note: This method will be called regardless of whether the change affects the component or not

If a setting in the configuration changes that affects the component, you would set its state here such that ComponentNeedsUpdate returns true next time, until the component is up-to-date again. Calling the base method is not required because it is empty.

SwitchEventArgs

In the HandleSwitch and Callback methods, the SwitchEventArgs, which carry information of why the switch occurred, are available. Available information can be:

  • The SwitchSource, which specifies where the switch originated from
  • The requested theme
  • The time when the theme switch was queued

Logging

The component has an NLog Logger available to use, reachable via

  • Logger.Info(string message) or Logger.Info(Exception ex, string message) for info messages
  • Logger.Warn(string message) or Logger.Warn(Exception ex, string message) for warning that are non critical
  • Logger.Error(string message) or Logger.Error(Exception ex, string message) for errors
  • Logger.Debug(string message) or Logger.Debug(Exception ex, string message) for debugging purposes (not shown in prod by default)

You should also call Logger.Info at the end of HandleSwitch, including

  • what the theme is now
  • what it was before
  • what the mode is

Example taken from log file:

2021-10-04 07:35:12 | Info | AppsSwitch.HandleSwitch: update info - previous: Dark, current: Light, mode: Switch 

Settings Object

Each component can either specify a settings object, or if it does not need custom settings, pass object into the generic type. Example:

  • with settings: internal class MyComponentSwitch : BaseComponent<MyComponentSwitchSettings>
  • without settings: internal class WallpaperSwitch : BaseComponent<object>

All settings objects must be placed in AutoDarkModeLib\ComponentSettings\Addons (create directory if doesn't exist).

A settings object inherits from either BaseSettings or BaseSettingsEnabled, if the component needs to be enabled by default. Please only use the latter if a prior agreement with the maintainers was reached.

Create a new class that represent your settings. Please follow the convention of the other modules.

Mode

Usually, for setting the switch modes, we use a Mode enum that contains the following values:

public enum Mode
{   
    Switch = 0, // meant to be used if the component should run when a theme switch is invoked
    LightOnly = 1, // meant to be used if the component should always use the dark state
    DarkOnly = 2 // meant to be used if the component should always use the light state
};

Example:

  • The component that switches the windows app theme is set to Mode.Switch if the user wants to have the windows app theme switch on a day/night trigger.
  • When it is set to Mode.DarkOnly, it will call its logic to switch to dark whenever necessary
  • When it is set to Mode.LightOnly, it will call its logic to switch to light whenever necessary

A really simple example of a ComponentNeedsUpdate that makes use of this convention:

    private Theme currentComponentTheme;
    ...
    public override bool ComponentNeedsUpdate(Theme newTheme)
    {
        if (Settings.Component.Mode == Mode.DarkOnly && currentComponentTheme != Theme.Dark)
        {
            return true;
        }
        else if (Settings.Component.Mode == Mode.LightOnly && currentComponentTheme != Theme.Light)
        {
            return true;
        }
        else if (Settings.Component.Mode == Mode.Switch && currentComponentTheme != newTheme)
        {
            return true;
        }
        return false;
    }

Other settings

For all other settings, you are free to create whatever your settings .cs file. You can add classes there if necessary and init them inside your custom settings object. However, a settings object must not have public methods, only properties! All public properties will get serialized and deserialized.

Accessing the settings object

The settings object can be reached in the component via Settings.Component.{whatever properties you have defined}

Adding a settings object to the config:

Open the AutoDarkModeConfig\AdmConfig.cs file

Add your component to the Addons object like so:

public class Addons
{
    public Addons()
    {
        MyComponentSwitchSettings = new BaseSettings<MyComponentSwitchSettings>();
    }
    //put your custom settings here!
    public BaseSettings<MyComponentSwitchSettings> MyComponentSwitch { get; set; }
}

Registering a component

You can now add your component to the component manager by opening AutoDarkModeSvc\Core\ComponentManager.cs

You will need to register it in three places.

  • Create a private readonly field with the component's interface ISwitchComponent and initialize it as a field
    ...
    //Others!
    private readonly ISwitchComponent AppsSwitch = new AppsSwitch();
    private readonly ISwitchComponent ColorFilterSwitch = new ColorFilterSwitch();
    ...
    //Yours!
    private readonly ISwitchComponent MyComponentSwitch = new MyComponentSwitchSwitch();
  • Add it to the UpdateSettings method
  public void UpdateSettings() 
  {
     //you will also find other components here like
     AppsSwitch.UpdateSettingsState(Builder.Config.AppsSwitch);
     //Add yours below, this is mandatory to receive updates when the settings change
     //Don't forget the Addons subclass!
     MyComponentSwitch.UpdateSettingsState(Builder.Config.Addons.MyComponentSwitch);
  }
  • Add it to the list of enabled components
    ComponentManager()
    {
        Builder = AdmConfigBuilder.Instance();
        Components = new List<ISwitchComponent>
        {
            AppsSwitch,
            ...
            //Yours here
            MyComponentSwitch
        };
        UpdateSettings();
    }

Finishing up

That's it, your component is now registered. Please check if it updates correctly and works without errors before submitting it!