Theme, colour, and presentation architecture

This page documents how OmegaT resolves colours and other presentation attributes for segment markers (active source, untranslated, fuzzy match highlights, and so on), and how Theme, Styles, and Preferences layer on top of each other. It exists to give contributors a map of the existing mechanism before proposing changes to it (for example, extending it to cover formatting, audio, or accessibility labels; see ADR 2026003).

There are three layers, resolved in order: Theme default, Styles registry, Preferences override.

1. Theme: UIDefaults keys under the OmegaT.* namespace

A Theme is a LookAndFeel implementation, installed via UIDesignManager.setTheme(String lafClassName). In addition to the usual Swing LaF keys, an OmegaT theme also seeds a set of application-specific colours as UIDefaults entries under the OmegaT. prefix, for example OmegaT.activeSource, OmegaT.untranslated, OmegaT.terminology.

Concretely, DefaultFlatDarkTheme.getDefaults() builds on FlatDarkLaf, then calls a private setDarkDefaults(UIDefaults) method that does, among other things:

defaults.put("OmegaT.activeSource", new Color(0x287128));
defaults.put("OmegaT.untranslated", new Color(0x4d4daa));
defaults.put("OmegaT.terminology", new Color(0x8f5500));
// ...

DefaultFlatLightTheme (light) does the equivalent for the light variant, in the theme Gradle module. DefaultFlatTheme (in src/main/java) is a shared base used by both — it supplies the newUI.* icon keys and the adjustRGB helper, not the marker colours themselves. Each installed theme is free to define its own values for every OmegaT.* key; nothing requires a theme to supply all of them.

Fallback: bundled colour scheme properties

If, after a theme is installed, UIManager.getColor("OmegaT.source") is still null (the theme did not define the OmegaT-specific keys at all — relevant for third-party theme plugins), UIDesignManager.initialize() falls back to loading src/main/resources/org/omegat/ColorScheme_dark.properties or ColorScheme_light.properties via ResourcesUtil.getBundleColorProperties(style) and UIDesignManager.loadDefaultColors. This is a safety net for incomplete themes, not the primary path for the bundled themes.

Theme plugin constraint

Theme plugins register their LaF class with the shared UIManager, which requires global visibility. This is why theme plugins share one ClassLoader rather than getting per-plugin isolation; see ADR 2025012 for the full rationale.

2. Registry: org.omegat.util.gui.Styles (EditorColor enum)

Application code never reads UIManager.getColor("OmegaT.xxx") directly for marker colours. It goes through Styles.EditorColor, an enum where each constant corresponds to one marker and resolves its default from the active theme at construction time:

COLOR_ACTIVE_SOURCE(OStrings.getString("COLOR_ACTIVE_SOURCE"),
        UIManager.getColor("OmegaT.activeSource")),
COLOR_UNTRANSLATED(OStrings.getString("COLOR_UNTRANSLATED"),
        UIManager.getColor("OmegaT.untranslated")),

A few constants (for example COLOR_PROJECT_FILES_PROGRESS_LOW) use a three-argument constructor that supplies a hard-coded hex fallback if the UIManager key is absent, instead of depending on the theme having set it. A handful of constants (for example COLOR_ACTIVE_SOURCE_FG, COLOR_GLOSSARY_SOURCE) have no theme-backed default at all and rely entirely on a Preferences value or remain null until one is set.

EditorColor is a registry, not just a lookup: at construction it also calls setColorFromPreference(), which is where layer 3 comes in.

3. Preferences override

Each EditorColor constant stores the user override under a Preferences key equal to its own enum name (Preferences.getPreferenceDefault(name(), null), for example the preference key for COLOR_ACTIVE_SOURCE is literally "COLOR_ACTIVE_SOURCE"). If a preference value is present and is not the sentinel "__DEFAULT__", it overrides the theme-derived colour. setColor(Color) is the write path used by the Colours preference panel (org.omegat.gui.preferences.view.CustomColorSelectionController / AppearanceController); passing the theme’s own default writes back the "__DEFAULT__" sentinel rather than a literal colour, so switching themes later is not masked by a stale override.

Resolution order, summarized

Theme (UIDefaults "OmegaT.*")
   -> fallback: ColorScheme_{dark,light}.properties  (only if theme omitted a key)
-> Styles.EditorColor default (read once, at enum construction, from UIManager)
-> Preferences override (checked at construction, and whenever setColor() is called)

Because EditorColor reads its default once per JVM at enum construction, changing the active theme at runtime does not retroactively change EditorColor defaults already resolved; theme switches are applied on restart (see switch_colour_theme.js script documentation for the user-facing workaround of a scripted theme swap).

Formatting attributes already exist, independent of colour

Styles.createAttributeSet(...) already accepts bold, italic, strikethrough, and underline flags in addition to foreground/background colour, and returns a Swing AttributeSet used to render segment text:

public static AttributeSet createAttributeSet(Color fg, Color bg,
        Boolean bold, Boolean italic, Boolean strikethrough, Boolean underline)

This means the formatting channel is not new plumbing to invent; it is already wired at the AttributeSet level. What does not exist yet is a per-marker-category configuration for these flags (comparable to what EditorColor provides for colour) or a Theme/Preferences resolution chain for them. Any proposal to add formatting as a configurable channel should extend EditorColor (or a sibling registry following the same three-layer pattern) to also carry these flags, rather than adding a separate formatting mechanism.

What is not covered by this mechanism

  • Audio/sound cues and accessibility labels/descriptions have no equivalent registry today; there is no OmegaT.* UIDefaults convention or Styles enum for either.

  • The chain above governs colour and, at the AttributeSet level, text formatting. It says nothing about how a value is synchronised or persisted beyond the single Preferences.setPreference(name(), ...) call — that is a separate, already-solved concern (see ADR 2026002 for the Preferences store’s own testability architecture, which this page does not depend on or duplicate).