Themes: selectable on-device presets (Custom / BASIC 80s / Terminal)

Add a Theme selector in Display Options that cycles three presets with a live
preview: Custom (CPS-editable, default), BASIC 80s (Commodore 64 light-blue on
blue) and Terminal (green-on-black night / black-on-green day). Presets fill all
theme items for a cohesive look.

The selection is stored in the codeplug custom-data area (new THEME_PRESET
block), which is migration-safe (missing block -> Custom) and needs no settings
reset. CPS theme edits only ever change the Custom theme. Documented in MANUAL.md
and the README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcus Kida 2026-07-12 14:41:08 +02:00
parent 5ac77c5ae9
commit 8c9828e386
7 changed files with 166 additions and 16 deletions

View file

@ -7,6 +7,11 @@ single running list until the first versioned release.
## Unreleased
- **Theme presets** — selectable on the radio in Display Options → "Theme":
**Custom** (CPS-editable, default), **BASIC 80s** (Commodore 64 blue/light-blue),
and **Terminal** (green-on-black night / black-on-green day). The choice is stored
in the codeplug custom-data area (migration-safe; defaults to Custom), and CPS
theme edits only ever change Custom.
- **Fix:** green LED **flickering** between the red flashes while muted during a DMR
QSO. The DMR RX handler re-asserts the green LED as soon as it reads it off, racing
the main-loop mute clear; the green LED is now **held off at the driver** for the

View file

@ -58,6 +58,17 @@ Optionally dims the backlight after inactivity to **100 / 50 / 30 / 15 %** of th
configured brightness (only in **Auto** backlight mode). Any activity restores full
brightness. Set it in **Menu → Options → Display Options → Screen Dim**.
### Theme presets — 🟢 [FreeTRX]
Pick a colour theme on the radio at **Menu → Options → Display Options → "Theme"**
(◀/▶ to change; the screen updates live):
- **Custom** — the theme edited via the CPS or the on-radio colour editor (default).
- **BASIC 80s** — a Commodore-64 look (light blue on blue).
- **Terminal** — a green terminal (green-on-black at night, black-on-green by day).
Editing colours in the CPS only affects **Custom**; the two built-in presets are
fixed. Every element (S-meter, frequencies, menus) follows the selected theme.
### Main-menu icons — 🟢 [FreeTRX]
Each top-level menu entry has a small icon next to it.

View file

@ -353,6 +353,7 @@ typedef enum
CODEPLUG_CUSTOM_DATA_TYPE_SATELLITE_TLE,
CODEPLUG_CUSTOM_DATA_TYPE_THEME_DAY,
CODEPLUG_CUSTOM_DATA_TYPE_THEME_NIGHT,
CODEPLUG_CUSTOM_DATA_TYPE_THEME_PRESET, // FreeTRX: 1 byte, selected theme preset (themePreset_t)
} CodeplugCustomDataType_t;

View file

@ -269,8 +269,21 @@ void displayDrawFrequencySevenSeg(int16_t x, int16_t y, int16_t cellW, int16_t c
#endif
#if defined(HAS_COLOURS)
// FreeTRX selectable theme presets. CUSTOM is the CPS-editable theme (default);
// the others are fixed palettes baked into the firmware.
typedef enum
{
THEME_PRESET_CUSTOM = 0, // editable via CPS / on-device colour editor
THEME_PRESET_BASIC80S, // Commodore 64 BASIC (light blue on blue)
THEME_PRESET_TERMINAL, // green terminal (green-on-black night / black-on-green day)
NUM_THEME_PRESETS
} themePreset_t;
void themeInitToDefaultValues(DayTime_t daytime, bool invert);
void themeInit(bool SPIFlashAvailable);
void themeApplyPreset(uint8_t preset); // rebuild themeItems[] for the given preset
uint8_t themeGetPreset(void); // read the stored preset (default CUSTOM)
void themeSetPreset(uint8_t preset, bool save); // apply, and optionally persist to the codeplug
void displayThemeApply(themeItem_t fgItem, themeItem_t bgItem);
void displayThemeResetToDefault(void);
bool displayThemeIsForegroundColourEqualTo(themeItem_t fgItem);

View file

@ -1510,29 +1510,105 @@ void themeInitToDefaultValues(DayTime_t daytime, bool invert)
themeItems[daytime][THEME_ITEM_FG_BD_COLOUR] = PLATFORM_COLOUR_FORMAT_SWAP_BYTES(RGB888_TO_PLATFORM_COLOUR_FORMAT(0xFF0000U));
}
void themeInit(bool SPIFlashAvailable)
// Set every theme item to fgRGB, then override the background items with bgRGB, for
// a cohesive single-palette look. The selected-menu-item background is intentionally
// left as the foreground colour so the highlighted row still inverts.
static void themeSetAllItems(DayTime_t daytime, uint32_t fgRGB, uint32_t bgRGB)
{
uint16_t fg = PLATFORM_COLOUR_FORMAT_SWAP_BYTES(RGB888_TO_PLATFORM_COLOUR_FORMAT(fgRGB));
uint16_t bg = PLATFORM_COLOUR_FORMAT_SWAP_BYTES(RGB888_TO_PLATFORM_COLOUR_FORMAT(bgRGB));
for (int i = 0; i < THEME_ITEM_MAX; i++)
{
themeItems[daytime][i] = fg;
}
themeItems[daytime][THEME_ITEM_BG] = bg;
themeItems[daytime][THEME_ITEM_BG_SPLASHSCREEN] = bg;
themeItems[daytime][THEME_ITEM_BG_NOTIFICATION] = bg;
themeItems[daytime][THEME_ITEM_BG_MENU_NAME] = bg;
themeItems[daytime][THEME_ITEM_BG_HEADER_TEXT] = bg;
// THEME_ITEM_BG_MENU_ITEM_SELECTED intentionally left = fg (inverts the selected row).
}
void themeApplyPreset(uint8_t preset)
{
// Start from the monochrome defaults, then overlay the chosen preset.
themeInitToDefaultValues(NIGHT, true);
themeInitToDefaultValues(DAY, false);
// Read and apply user's theme, if any
if (SPIFlashAvailable)
switch (preset)
{
case THEME_PRESET_BASIC80S: // Commodore 64 BASIC: light blue on blue (day & night)
themeSetAllItems(DAY, 0x7C71D5, 0x3E31A2);
themeSetAllItems(NIGHT, 0x7C71D5, 0x3E31A2);
break;
case THEME_PRESET_TERMINAL:
themeSetAllItems(DAY, 0x000000, 0x00A000); // black on green
themeSetAllItems(NIGHT, 0x00E000, 0x000000); // green on black
break;
case THEME_PRESET_CUSTOM:
default:
// Overlay the user's Custom theme from the codeplug, if present (CPS edits
// only ever touch this).
{
uint16_t themingTmp[THEME_ITEM_MAX];
if (codeplugGetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_DAY, (uint8_t *) &themingTmp))
{
memcpy(&themeItems[DAY], &themingTmp, sizeof(themingTmp));
foregroundColour = themeItems[DAY][THEME_ITEM_FG_DEFAULT];
backgroundColour = themeItems[DAY][THEME_ITEM_BG];
}
if (codeplugGetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_NIGHT, (uint8_t *) &themingTmp))
{
memcpy(&themeItems[NIGHT], &themingTmp, sizeof(themingTmp));
}
}
break;
}
foregroundColour = themeItems[DAY][THEME_ITEM_FG_DEFAULT];
backgroundColour = themeItems[DAY][THEME_ITEM_BG];
}
uint8_t themeGetPreset(void)
{
uint8_t buf[4] = { THEME_PRESET_CUSTOM, 0, 0, 0 };
if (codeplugGetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_PRESET, buf))
{
return (buf[0] < NUM_THEME_PRESETS) ? buf[0] : THEME_PRESET_CUSTOM;
}
return THEME_PRESET_CUSTOM;
}
void themeSetPreset(uint8_t preset, bool save)
{
if (preset >= NUM_THEME_PRESETS)
{
preset = THEME_PRESET_CUSTOM;
}
themeApplyPreset(preset);
if (save)
{
uint8_t buf[1] = { preset };
codeplugSetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_PRESET, buf, sizeof(buf));
}
}
void themeInit(bool SPIFlashAvailable)
{
if (SPIFlashAvailable)
{
themeApplyPreset(themeGetPreset());
}
else
{
themeInitToDefaultValues(NIGHT, true);
themeInitToDefaultValues(DAY, false);
}
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT_OVERRIDE) && (uiDataGlobal.daytimeOverridden == NIGHT))
{

View file

@ -104,6 +104,9 @@ enum
DISPLAY_SHOW_DISTANCE,
DISPLAY_DMR_LAST_TALKER_ON_SCREEN,
DISPLAY_SCREEN_DIM,
#if defined(HAS_COLOURS)
DISPLAY_THEME_PRESET,
#endif
NUM_DISPLAY_MENU_ITEMS
};
@ -269,6 +272,18 @@ static void updateScreen(bool isFirstRun)
}
break;
#if defined(HAS_COLOURS)
case DISPLAY_THEME_PRESET:
{
// Preset names are plain literals -> rightSideVar (voice-safe).
const char *presetNames[] = { "Custom", "BASIC 80s", "Terminal" };
uint8_t p = themeGetPreset();
leftSide = "Theme";
snprintf(rightSideVar, SCREEN_LINE_BUFFER_SIZE, "%s", presetNames[(p < NUM_THEME_PRESETS) ? p : 0]);
}
break;
#endif
#if ! defined(PLATFORM_GD77S)
case DISPLAY_AUTO_NIGHT:
leftSide = currentLanguage->auto_night;
@ -630,6 +645,19 @@ static void handleEvent(uiEvent_t *ev)
}
break;
#if defined(HAS_COLOURS)
case DISPLAY_THEME_PRESET:
{
uint8_t p = themeGetPreset();
if (p < (NUM_THEME_PRESETS - 1))
{
themeSetPreset(p + 1, true); // apply + persist, live preview
displayThemeResetToDefault();
}
}
break;
#endif
#if ! defined(PLATFORM_GD77S)
case DISPLAY_AUTO_NIGHT:
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT) == false)
@ -839,6 +867,19 @@ static void handleEvent(uiEvent_t *ev)
}
break;
#if defined(HAS_COLOURS)
case DISPLAY_THEME_PRESET:
{
uint8_t p = themeGetPreset();
if (p > THEME_PRESET_CUSTOM)
{
themeSetPreset(p - 1, true); // apply + persist, live preview
displayThemeResetToDefault();
}
}
break;
#endif
#if ! defined(PLATFORM_GD77S)
case DISPLAY_AUTO_NIGHT:
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT))

View file

@ -35,6 +35,9 @@ FreeTRX keeps OpenGD77's core (DMR/FM, hotspot, CPS compatibility) and adds:
- **7-segment "LED" frequency font** on the VFO and the SK1-held frequency view.
- **Screen dimming** — optionally dim the backlight to 100 / 50 / 30 / 15 % after
inactivity (Auto backlight mode); new setting in Display Options.
- **Theme presets** — pick a colour theme on the radio: **Custom** (CPS-editable,
default), **BASIC 80s** (Commodore 64), or **Terminal** (green-on-black). CPS edits
only affect Custom.
- **On-radio channel management** — create and delete channels directly from the
Channel quick menu, no PC/CPS required (see [Managing channels](#managing-channels-on-the-radio)).
- **Main-menu icons** next to each top-level entry.