DVX_GUI/apps/cpanel/cpanel.c

1095 lines
31 KiB
C

// ctrlpanel.c -- DVX Control Panel
//
// System configuration app with four tabs:
// Mouse -- scroll direction, double-click speed, acceleration
// Colors -- all 18 system colors, theme load/save
// Desktop -- wallpaper image (stretch mode)
// Video -- resolution and color depth
//
// Changes preview live. OK saves to DVX.INI, Cancel reverts to the
// state captured when the control panel was opened.
#include "dvxApp.h"
#include "dvxDialog.h"
#include "dvxPrefs.h"
#include "dvxWidget.h"
#include "dvxWm.h"
#include "platform/dvxPlatform.h"
#include "shellApp.h"
#include "thirdparty/stb_ds.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
// ============================================================
// Constants
// ============================================================
#define CP_WIN_W 460
#define CP_WIN_H 340
#define WPAPER_DIR "CONFIG/WPAPER"
#define THEME_DIR "CONFIG/THEMES"
#define THEME_EXT ".THM"
// ============================================================
// App descriptor
// ============================================================
AppDescriptorT appDescriptor = {
.name = "Control Panel",
.hasMainLoop = false,
.multiInstance = false,
.stackSize = SHELL_STACK_DEFAULT,
.priority = TS_PRIORITY_NORMAL
};
// ============================================================
// Entry types for dynamic lists
// ============================================================
typedef struct {
char name[64];
char path[260];
} FileEntryT;
// ============================================================
// Module state
// ============================================================
static DxeAppContextT *sCtx = NULL;
static AppContextT *sAc = NULL;
static WindowT *sWin = NULL;
// Saved state for Cancel
static uint8_t sSavedColorRgb[ColorCountE][3];
static int32_t sSavedWheelDir;
static int32_t sSavedDblClick;
static int32_t sSavedAccel;
static int32_t sSavedVideoW;
static int32_t sSavedVideoH;
static int32_t sSavedVideoBpp;
static WallpaperModeE sSavedWpMode;
// Mouse tab widgets
static WidgetT *sWheelDrop = NULL;
static WidgetT *sDblClickSldr = NULL;
static WidgetT *sDblClickLbl = NULL;
static WidgetT *sAccelDrop = NULL;
static WidgetT *sDblTestLbl = NULL;
// Colors tab widgets
static WidgetT *sColorList = NULL;
static WidgetT *sRedSldr = NULL;
static WidgetT *sGreenSldr = NULL;
static WidgetT *sBlueSldr = NULL;
static WidgetT *sRedLbl = NULL;
static WidgetT *sGreenLbl = NULL;
static WidgetT *sBlueLbl = NULL;
static WidgetT *sColorSwatch = NULL;
// Desktop tab widgets
static WidgetT *sWallpaperLbl = NULL;
static WidgetT *sWpaperList = NULL;
static WidgetT *sWpModeDrop = NULL;
static char sWallpaperPath[260];
static FileEntryT *sWpaperEntries = NULL; // stb_ds dynamic array
static const char **sWpaperLabels = NULL; // stb_ds dynamic array
// Video tab
static const VideoModeInfoT *sVideoModes = NULL; // points into ctx, not owned
static int32_t sVideoCount = 0;
static const char **sVideoLabels = NULL; // stb_ds dynamic array
static WidgetT *sVideoList = NULL;
// Theme list
static FileEntryT *sThemeEntries = NULL; // stb_ds dynamic array
static const char **sThemeLabels = NULL; // stb_ds dynamic array
static WidgetT *sThemeList = NULL;
// ============================================================
// Prototypes
// ============================================================
int32_t appMain(DxeAppContextT *ctx);
static void buildMouseTab(WidgetT *page);
static void buildColorsTab(WidgetT *page);
static void buildDesktopTab(WidgetT *page);
static void buildVideoTab(WidgetT *page);
static int32_t mapAccelName(const char *name);
static const char *mapAccelValue(int32_t val);
static void onAccelChange(WidgetT *w);
static void onApplyTheme(WidgetT *w);
static void onBrowseTheme(WidgetT *w);
static void onResetColors(WidgetT *w);
static void onApplyWallpaper(WidgetT *w);
static void onWallpaperMode(WidgetT *w);
static void onCancel(WidgetT *w);
static void onChooseWallpaper(WidgetT *w);
static void onClearWallpaper(WidgetT *w);
static void onClose(WindowT *win);
static void onColorSelect(WidgetT *w);
static void onColorSlider(WidgetT *w);
static void onDblClickSlider(WidgetT *w);
static void onDblClickTest(WidgetT *w);
static void onOk(WidgetT *w);
static void onSaveTheme(WidgetT *w);
static void onVideoApply(WidgetT *w);
static void onWheelChange(WidgetT *w);
static void saveSnapshot(void);
static void restoreSnapshot(void);
static void scanWallpapers(void);
static void scanThemes(void);
static void updateColorSliders(void);
static void updateDblClickLabel(void);
static void updateSwatch(void);
// ============================================================
// buildColorsTab
// ============================================================
static void buildColorsTab(WidgetT *page) {
// Color list on the left, sliders on the right
WidgetT *hbox = wgtHBox(page);
hbox->weight = 100;
hbox->spacing = wgtPixels(8);
// Left side: color name list + theme controls
WidgetT *leftVbox = wgtVBox(hbox);
leftVbox->weight = 50;
leftVbox->spacing = wgtPixels(4);
wgtLabel(leftVbox, "System Colors:");
// Build color name list
static const char *colorNames[ColorCountE];
for (int32_t i = 0; i < ColorCountE; i++) {
colorNames[i] = dvxColorLabel((ColorIdE)i);
}
sColorList = wgtListBox(leftVbox);
sColorList->weight = 100;
sColorList->onChange = onColorSelect;
wgtListBoxSetItems(sColorList, colorNames, ColorCountE);
wgtListBoxSetSelected(sColorList, 0);
// Theme row
WidgetT *themeRow = wgtHBox(leftVbox);
themeRow->spacing = wgtPixels(4);
wgtLabel(themeRow, "Theme:");
scanThemes();
sThemeList = wgtDropdown(themeRow);
sThemeList->weight = 100;
wgtDropdownSetItems(sThemeList, sThemeLabels, arrlen(sThemeEntries));
WidgetT *loadBtn = wgtButton(themeRow, "Apply");
loadBtn->onClick = onApplyTheme;
loadBtn->prefW = wgtPixels(60);
WidgetT *browseBtn = wgtButton(themeRow, "Load...");
browseBtn->onClick = onBrowseTheme;
browseBtn->prefW = wgtPixels(60);
WidgetT *saveBtn = wgtButton(themeRow, "Save As...");
saveBtn->onClick = onSaveTheme;
saveBtn->prefW = wgtPixels(70);
WidgetT *resetBtn = wgtButton(themeRow, "Reset");
resetBtn->onClick = onResetColors;
resetBtn->prefW = wgtPixels(60);
// Right side: RGB sliders in a non-weighted inner box so they stay
// at natural size. The outer box absorbs extra vertical space.
WidgetT *rightVbox = wgtVBox(hbox);
rightVbox->weight = 50;
WidgetT *sliderBox = wgtVBox(rightVbox);
sliderBox->spacing = wgtPixels(4);
wgtLabel(sliderBox, "Red:");
sRedSldr = wgtSlider(sliderBox, 0, 255);
sRedSldr->onChange = onColorSlider;
sRedLbl = wgtLabel(sliderBox, "0");
wgtLabelSetAlign(sRedLbl, AlignEndE);
wgtLabel(sliderBox, "Green:");
sGreenSldr = wgtSlider(sliderBox, 0, 255);
sGreenSldr->onChange = onColorSlider;
sGreenLbl = wgtLabel(sliderBox, "0");
wgtLabelSetAlign(sGreenLbl, AlignEndE);
wgtLabel(sliderBox, "Blue:");
sBlueSldr = wgtSlider(sliderBox, 0, 255);
sBlueSldr->onChange = onColorSlider;
sBlueLbl = wgtLabel(sliderBox, "0");
wgtLabelSetAlign(sBlueLbl, AlignEndE);
wgtLabel(sliderBox, "Preview:");
sColorSwatch = wgtCanvas(sliderBox, 64, 24);
// Absorb remaining vertical space below the sliders
wgtSpacer(rightVbox)->weight = 100;
updateColorSliders();
}
// ============================================================
// buildDesktopTab
// ============================================================
static void buildDesktopTab(WidgetT *page) {
wgtLabel(page, "Wallpaper:");
scanWallpapers();
sWpaperList = wgtListBox(page);
sWpaperList->weight = 100;
sWpaperList->onDblClick = onApplyWallpaper;
wgtListBoxSetItems(sWpaperList, sWpaperLabels, arrlen(sWpaperEntries));
sWallpaperLbl = wgtLabel(page, sWallpaperPath[0] ? sWallpaperPath : "(none)");
WidgetT *btnRow = wgtHBox(page);
btnRow->spacing = wgtPixels(8);
WidgetT *applyBtn = wgtButton(btnRow, "Apply");
applyBtn->onClick = onApplyWallpaper;
applyBtn->prefW = wgtPixels(90);
WidgetT *chooseBtn = wgtButton(btnRow, "Browse...");
chooseBtn->onClick = onChooseWallpaper;
chooseBtn->prefW = wgtPixels(90);
WidgetT *clearBtn = wgtButton(btnRow, "Clear");
clearBtn->onClick = onClearWallpaper;
clearBtn->prefW = wgtPixels(90);
wgtSpacer(btnRow)->weight = 100;
wgtLabel(btnRow, "Mode:");
static const char *modeItems[] = {"Stretch", "Tile", "Center"};
sWpModeDrop = wgtDropdown(btnRow);
sWpModeDrop->onChange = onWallpaperMode;
wgtDropdownSetItems(sWpModeDrop, modeItems, 3);
wgtDropdownSetSelected(sWpModeDrop, (int32_t)sAc->wallpaperMode);
}
// ============================================================
// buildMouseTab
// ============================================================
static void buildMouseTab(WidgetT *page) {
page->spacing = wgtPixels(6);
// Scroll direction
WidgetT *wheelRow = wgtHBox(page);
wheelRow->spacing = wgtPixels(8);
wgtLabel(wheelRow, "Scroll Wheel:");
static const char *wheelItems[] = {"Normal", "Reversed"};
sWheelDrop = wgtDropdown(wheelRow);
sWheelDrop->weight = 100;
sWheelDrop->onChange = onWheelChange;
wgtDropdownSetItems(sWheelDrop, wheelItems, 2);
wgtDropdownSetSelected(sWheelDrop, sAc->wheelDirection < 0 ? 1 : 0);
wgtSpacer(page)->prefH = wgtPixels(4);
// Double-click speed
wgtLabel(page, "Double-Click Speed:");
WidgetT *dblRow = wgtHBox(page);
dblRow->spacing = wgtPixels(8);
wgtLabel(dblRow, "Fast");
sDblClickSldr = wgtSlider(dblRow, 200, 900);
sDblClickSldr->weight = 100;
sDblClickSldr->onChange = onDblClickSlider;
int32_t dblMs = prefsGetInt("mouse", "doubleclick", 500);
wgtSliderSetValue(sDblClickSldr, dblMs);
wgtLabel(dblRow, "Slow ");
sDblClickLbl = wgtLabel(dblRow, "");
sDblClickLbl->prefW = wgtPixels(52);
updateDblClickLabel();
wgtSpacer(page)->prefH = wgtPixels(4);
// Acceleration
WidgetT *accelRow = wgtHBox(page);
accelRow->spacing = wgtPixels(8);
wgtLabel(accelRow, "Acceleration:");
static const char *accelItems[] = {"Off", "Low", "Medium", "High"};
sAccelDrop = wgtDropdown(accelRow);
sAccelDrop->weight = 100;
sAccelDrop->onChange = onAccelChange;
wgtDropdownSetItems(sAccelDrop, accelItems, 4);
const char *accelStr = prefsGetString("mouse", "acceleration", "medium");
if (strcmp(accelStr, "off") == 0) {
wgtDropdownSetSelected(sAccelDrop, 0);
} else if (strcmp(accelStr, "low") == 0) {
wgtDropdownSetSelected(sAccelDrop, 1);
} else if (strcmp(accelStr, "high") == 0) {
wgtDropdownSetSelected(sAccelDrop, 3);
} else {
wgtDropdownSetSelected(sAccelDrop, 2);
}
wgtSpacer(page)->prefH = wgtPixels(4);
// Double-click test area
WidgetT *testRow = wgtHBox(page);
testRow->spacing = wgtPixels(8);
WidgetT *testBtn = wgtButton(testRow, "Test Area");
testBtn->onDblClick = onDblClickTest;
testBtn->prefW = wgtPixels(100);
sDblTestLbl = wgtLabel(testRow, "Double-click the button to test");
}
// ============================================================
// buildVideoTab
// ============================================================
static void buildVideoTab(WidgetT *page) {
wgtLabel(page, "Available Video Modes:");
// Read modes enumerated at init time
sVideoModes = dvxGetVideoModes(sAc, &sVideoCount);
// Build label strings
arrsetlen(sVideoLabels, 0);
static char (*labelBufs)[48] = NULL;
arrfree(labelBufs);
labelBufs = NULL;
arrsetlen(labelBufs, sVideoCount);
for (int32_t i = 0; i < sVideoCount; i++) {
const char *depthName;
switch (sVideoModes[i].bpp) {
case 8: depthName = "256 colors"; break;
case 15: depthName = "32 thousand colors"; break;
case 16: depthName = "65 thousand colors"; break;
case 24: depthName = "16 million colors"; break;
case 32: depthName = "16 million colors+"; break;
default: depthName = ""; break;
}
snprintf(labelBufs[i], 48, "%ldx%ld %s", (long)sVideoModes[i].w, (long)sVideoModes[i].h, depthName);
arrput(sVideoLabels, (const char *)labelBufs[i]);
}
sVideoList = wgtListBox(page);
sVideoList->weight = 100;
sVideoList->onDblClick = onVideoApply;
wgtListBoxSetItems(sVideoList, sVideoLabels, sVideoCount);
// Select the current mode
for (int32_t i = 0; i < sVideoCount; i++) {
if (sVideoModes[i].w == sAc->display.width &&
sVideoModes[i].h == sAc->display.height &&
sVideoModes[i].bpp == sAc->display.format.bitsPerPixel) {
wgtListBoxSetSelected(sVideoList, i);
break;
}
}
WidgetT *applyBtn = wgtButton(page, "Apply Mode");
applyBtn->onClick = onVideoApply;
applyBtn->prefW = wgtPixels(100);
}
// ============================================================
// mapAccelName / mapAccelValue
// ============================================================
static int32_t mapAccelName(const char *name) {
if (strcmp(name, "off") == 0) return 10000;
if (strcmp(name, "low") == 0) return 100;
if (strcmp(name, "medium") == 0) return 64;
if (strcmp(name, "high") == 0) return 32;
return 64;
}
static const char *mapAccelValue(int32_t idx) {
switch (idx) {
case 0: return "off";
case 1: return "low";
case 2: return "medium";
case 3: return "high";
default: return "medium";
}
}
// ============================================================
// Callbacks -- Mouse tab
// ============================================================
static void onWheelChange(WidgetT *w) {
int32_t sel = wgtDropdownGetSelected(w);
int32_t dir = (sel == 1) ? -1 : 1;
int32_t dbl = wgtSliderGetValue(sDblClickSldr);
const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop));
int32_t accelVal = mapAccelName(accelName);
dvxSetMouseConfig(sAc, dir, dbl, accelVal);
}
static void onDblClickSlider(WidgetT *w) {
(void)w;
updateDblClickLabel();
int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1;
int32_t dbl = wgtSliderGetValue(sDblClickSldr);
const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop));
int32_t accelVal = mapAccelName(accelName);
dvxSetMouseConfig(sAc, dir, dbl, accelVal);
}
static void onDblClickTest(WidgetT *w) {
(void)w;
static int32_t count = 0;
count++;
static char buf[48];
snprintf(buf, sizeof(buf), "Double-click detected! (%ld)", (long)count);
wgtSetText(sDblTestLbl, buf);
}
static void onAccelChange(WidgetT *w) {
int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1;
int32_t dbl = wgtSliderGetValue(sDblClickSldr);
const char *accelName = mapAccelValue(wgtDropdownGetSelected(w));
int32_t accelVal = mapAccelName(accelName);
dvxSetMouseConfig(sAc, dir, dbl, accelVal);
}
// ============================================================
// Callbacks -- Colors tab
// ============================================================
static void onColorSelect(WidgetT *w) {
(void)w;
updateColorSliders();
}
static void onColorSlider(WidgetT *w) {
(void)w;
int32_t sel = wgtListBoxGetSelected(sColorList);
if (sel < 0 || sel >= ColorCountE) {
return;
}
uint8_t r = (uint8_t)wgtSliderGetValue(sRedSldr);
uint8_t g = (uint8_t)wgtSliderGetValue(sGreenSldr);
uint8_t b = (uint8_t)wgtSliderGetValue(sBlueSldr);
// Update label text
static char rBuf[8];
static char gBuf[8];
static char bBuf[8];
snprintf(rBuf, sizeof(rBuf), "%d", r);
snprintf(gBuf, sizeof(gBuf), "%d", g);
snprintf(bBuf, sizeof(bBuf), "%d", b);
wgtSetText(sRedLbl, rBuf);
wgtSetText(sGreenLbl, gBuf);
wgtSetText(sBlueLbl, bBuf);
dvxSetColor(sAc, (ColorIdE)sel, r, g, b);
updateSwatch();
}
static void onApplyTheme(WidgetT *w) {
(void)w;
int32_t sel = wgtDropdownGetSelected(sThemeList);
if (sel < 0 || sel >= arrlen(sThemeEntries)) {
return;
}
dvxLoadTheme(sAc, sThemeEntries[sel].path);
updateColorSliders();
}
static void onBrowseTheme(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Theme Files (*.thm)", "*.thm" },
{ "All Files (*.*)", "*.*" }
};
char path[260];
if (dvxFileDialog(sAc, "Load Theme", FD_OPEN, THEME_DIR, filters, 2, path, sizeof(path))) {
dvxLoadTheme(sAc, path);
updateColorSliders();
}
}
static void onSaveTheme(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Theme Files (*.thm)", "*.thm" },
{ "All Files (*.*)", "*.*" }
};
char path[260];
if (dvxFileDialog(sAc, "Save Theme", FD_SAVE, THEME_DIR, filters, 2, path, sizeof(path))) {
dvxSaveTheme(sAc, path);
scanThemes();
wgtDropdownSetItems(sThemeList, sThemeLabels, arrlen(sThemeEntries));
}
}
static void onResetColors(WidgetT *w) {
(void)w;
dvxResetColorScheme(sAc);
updateColorSliders();
}
// ============================================================
// Callbacks -- Desktop tab
// ============================================================
static void onApplyWallpaper(WidgetT *w) {
(void)w;
int32_t sel = wgtListBoxGetSelected(sWpaperList);
if (sel < 0 || sel >= arrlen(sWpaperEntries)) {
return;
}
if (dvxSetWallpaper(sAc, sWpaperEntries[sel].path)) {
snprintf(sWallpaperPath, sizeof(sWallpaperPath), "%s", sWpaperEntries[sel].path);
wgtSetText(sWallpaperLbl, sWallpaperPath);
} else {
dvxMessageBox(sAc, "Error", "Could not load wallpaper image.", MB_OK | MB_ICONERROR);
}
}
static void onChooseWallpaper(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Images (*.bmp;*.jpg;*.png)", "*.bmp;*.jpg;*.png" },
{ "All Files (*.*)", "*.*" }
};
char path[260];
if (dvxFileDialog(sAc, "Choose Wallpaper", FD_OPEN, "CONFIG/WPAPER", filters, 2, path, sizeof(path))) {
if (dvxSetWallpaper(sAc, path)) {
strncpy(sWallpaperPath, path, sizeof(sWallpaperPath) - 1);
sWallpaperPath[sizeof(sWallpaperPath) - 1] = '\0';
wgtSetText(sWallpaperLbl, sWallpaperPath);
} else {
dvxMessageBox(sAc, "Error", "Could not load wallpaper image.", MB_OK | MB_ICONERROR);
}
}
}
static void onClearWallpaper(WidgetT *w) {
(void)w;
dvxSetWallpaper(sAc, NULL);
sWallpaperPath[0] = '\0';
wgtSetText(sWallpaperLbl, "(none)");
}
static void onWallpaperMode(WidgetT *w) {
int32_t sel = wgtDropdownGetSelected(w);
if (sel >= 0 && sel <= 2) {
dvxSetWallpaperMode(sAc, (WallpaperModeE)sel);
}
}
// ============================================================
// Callbacks -- Video tab
// ============================================================
static int32_t sVideoConfirmResult = -1;
static void onVideoConfirmYes(WidgetT *w) {
(void)w;
sVideoConfirmResult = 1;
}
static void onVideoConfirmNo(WidgetT *w) {
(void)w;
sVideoConfirmResult = 0;
}
static void onVideoConfirmClose(WindowT *win) {
(void)win;
sVideoConfirmResult = 0;
}
static void onVideoApply(WidgetT *w) {
(void)w;
// Guard against re-entrancy: the confirmation dialog runs a nested
// dvxUpdate loop which can process pending events that call us again.
static bool sInProgress = false;
if (sInProgress) {
return;
}
sInProgress = true;
int32_t sel = wgtListBoxGetSelected(sVideoList);
if (sel < 0 || sel >= sVideoCount) {
sInProgress = false;
return;
}
const VideoModeInfoT *m = &sVideoModes[sel];
int32_t oldW = sAc->display.width;
int32_t oldH = sAc->display.height;
int32_t oldBpp = sAc->display.format.bitsPerPixel;
if (m->w == oldW && m->h == oldH && m->bpp == oldBpp) {
sInProgress = false;
return;
}
if (dvxChangeVideoMode(sAc, m->w, m->h, m->bpp) != 0) {
dvxMessageBox(sAc, "Error", "Failed to change video mode.", MB_OK | MB_ICONERROR);
sInProgress = false;
return;
}
// Confirmation with 10-second auto-revert countdown. Run a nested
// event loop that updates the dialog title each second. If the user
// clicks Yes the mode is kept; No or timeout reverts to the old mode.
#define CONFIRM_SECONDS 10
WindowT *confirmWin = dvxCreateWindowCentered(sAc, "Keep this mode?", 300, 100, false);
if (!confirmWin) {
dvxChangeVideoMode(sAc, oldW, oldH, oldBpp);
sInProgress = false;
return;
}
confirmWin->modal = true;
confirmWin->onClose = onVideoConfirmClose;
sAc->modalWindow = confirmWin;
WidgetT *root = wgtInitWindow(sAc, confirmWin);
root->spacing = wgtPixels(8);
static char msgBuf[80];
snprintf(msgBuf, sizeof(msgBuf), "Reverting in %d seconds...", CONFIRM_SECONDS);
WidgetT *msgLbl = wgtLabel(root, msgBuf);
WidgetT *btnRow = wgtHBox(root);
btnRow->align = AlignEndE;
btnRow->spacing = wgtPixels(8);
WidgetT *yesBtn = wgtButton(btnRow, "Yes");
yesBtn->prefW = wgtPixels(70);
yesBtn->onClick = onVideoConfirmYes;
WidgetT *noBtn = wgtButton(btnRow, "No");
noBtn->prefW = wgtPixels(70);
noBtn->onClick = onVideoConfirmNo;
dvxFitWindow(sAc, confirmWin);
sVideoConfirmResult = -1;
clock_t lastSecond = clock();
int32_t secondsLeft = CONFIRM_SECONDS;
while (sAc->running && secondsLeft > 0 && sVideoConfirmResult < 0) {
dvxUpdate(sAc);
clock_t now = clock();
if ((now - lastSecond) >= CLOCKS_PER_SEC) {
lastSecond = now;
secondsLeft--;
snprintf(msgBuf, sizeof(msgBuf), "Reverting in %ld seconds...", (long)secondsLeft);
wgtSetText(msgLbl, msgBuf);
}
}
sAc->modalWindow = NULL;
dvxDestroyWindow(sAc, confirmWin);
if (sVideoConfirmResult != 1) {
dvxChangeVideoMode(sAc, oldW, oldH, oldBpp);
}
sInProgress = false;
}
// ============================================================
// Callbacks -- OK / Cancel / Close
// ============================================================
static void onOk(WidgetT *w) {
(void)w;
// Save mouse settings
int32_t wheelSel = wgtDropdownGetSelected(sWheelDrop);
prefsSetString("mouse", "wheel", wheelSel == 1 ? "reversed" : "normal");
prefsSetInt("mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr));
prefsSetString("mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop)));
// Save colors to INI
for (int32_t i = 0; i < ColorCountE; i++) {
uint8_t r;
uint8_t g;
uint8_t b;
dvxGetColor(sAc, (ColorIdE)i, &r, &g, &b);
char val[16];
snprintf(val, sizeof(val), "%d,%d,%d", r, g, b);
prefsSetString("colors", dvxColorName((ColorIdE)i), val);
}
// Save desktop settings
if (sWallpaperPath[0]) {
prefsSetString("desktop", "wallpaper", sWallpaperPath);
} else {
prefsRemove("desktop", "wallpaper");
}
const char *modeStr = "stretch";
if (sAc->wallpaperMode == WallpaperTileE) {
modeStr = "tile";
} else if (sAc->wallpaperMode == WallpaperCenterE) {
modeStr = "center";
}
prefsSetString("desktop", "mode", modeStr);
// Save video settings
prefsSetInt("video", "width", sAc->display.width);
prefsSetInt("video", "height", sAc->display.height);
prefsSetInt("video", "bpp", sAc->display.format.bitsPerPixel);
prefsSave();
dvxDestroyWindow(sAc, sWin);
sWin = NULL;
}
static void onCancel(WidgetT *w) {
(void)w;
restoreSnapshot();
dvxDestroyWindow(sAc, sWin);
sWin = NULL;
}
static void onClose(WindowT *win) {
restoreSnapshot();
dvxDestroyWindow(sAc, win);
sWin = NULL;
}
// ============================================================
// saveSnapshot / restoreSnapshot
// ============================================================
static char sSavedWallpaperPath[260];
static void saveSnapshot(void) {
memcpy(sSavedColorRgb, sAc->colorRgb, sizeof(sSavedColorRgb));
sSavedWheelDir = sAc->wheelDirection;
sSavedDblClick = (int32_t)(sAc->dblClickTicks * 1000 / CLOCKS_PER_SEC);
sSavedAccel = 0;
sSavedVideoW = sAc->display.width;
sSavedVideoH = sAc->display.height;
sSavedVideoBpp = sAc->display.format.bitsPerPixel;
// Save the active wallpaper path and mode from the context
snprintf(sSavedWallpaperPath, sizeof(sSavedWallpaperPath), "%s", sAc->wallpaperPath);
snprintf(sWallpaperPath, sizeof(sWallpaperPath), "%s", sAc->wallpaperPath);
sSavedWpMode = sAc->wallpaperMode;
}
static void restoreSnapshot(void) {
// Restore colors
memcpy(sAc->colorRgb, sSavedColorRgb, sizeof(sSavedColorRgb));
dvxApplyColorScheme(sAc);
// Restore mouse
const char *accelStr = prefsGetString("mouse", "acceleration", "medium");
int32_t accelVal = mapAccelName(accelStr);
dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal);
// Restore video mode if changed
if (sAc->display.width != sSavedVideoW ||
sAc->display.height != sSavedVideoH ||
sAc->display.format.bitsPerPixel != sSavedVideoBpp) {
dvxChangeVideoMode(sAc, sSavedVideoW, sSavedVideoH, sSavedVideoBpp);
}
// Restore wallpaper mode and image
sAc->wallpaperMode = sSavedWpMode;
if (sSavedWallpaperPath[0]) {
dvxSetWallpaper(sAc, sSavedWallpaperPath);
} else {
dvxSetWallpaper(sAc, NULL);
}
}
// ============================================================
// scanThemes
// ============================================================
static void scanThemes(void) {
arrsetlen(sThemeEntries, 0);
arrsetlen(sThemeLabels, 0);
DIR *dir = opendir(THEME_DIR);
if (!dir) {
return;
}
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
char *dot = strrchr(ent->d_name, '.');
if (!dot || strcasecmp(dot, THEME_EXT) != 0) {
continue;
}
FileEntryT entry = {0};
int32_t nameLen = (int32_t)(dot - ent->d_name);
if (nameLen >= (int32_t)sizeof(entry.name)) {
nameLen = (int32_t)sizeof(entry.name) - 1;
}
memcpy(entry.name, ent->d_name, nameLen);
entry.name[nameLen] = '\0';
snprintf(entry.path, sizeof(entry.path), "%s/%s", THEME_DIR, ent->d_name);
arrput(sThemeEntries, entry);
}
closedir(dir);
// Build label array now that sThemeEntries is stable
for (int32_t i = 0; i < arrlen(sThemeEntries); i++) {
arrput(sThemeLabels, sThemeEntries[i].name);
}
}
// ============================================================
// scanWallpapers
// ============================================================
static void scanWallpapers(void) {
arrsetlen(sWpaperEntries, 0);
arrsetlen(sWpaperLabels, 0);
DIR *dir = opendir(WPAPER_DIR);
if (!dir) {
return;
}
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
char *dot = strrchr(ent->d_name, '.');
if (!dot) {
continue;
}
if (strcasecmp(dot, ".BMP") != 0 &&
strcasecmp(dot, ".JPG") != 0 &&
strcasecmp(dot, ".PNG") != 0) {
continue;
}
FileEntryT entry = {0};
snprintf(entry.name, sizeof(entry.name), "%s", ent->d_name);
snprintf(entry.path, sizeof(entry.path), "%s/%s", WPAPER_DIR, ent->d_name);
arrput(sWpaperEntries, entry);
}
closedir(dir);
// Build label array now that sWpaperEntries is stable
for (int32_t i = 0; i < arrlen(sWpaperEntries); i++) {
arrput(sWpaperLabels, sWpaperEntries[i].name);
}
}
// ============================================================
// updateColorSliders
// ============================================================
static void updateColorSliders(void) {
int32_t sel = wgtListBoxGetSelected(sColorList);
if (sel < 0 || sel >= ColorCountE) {
return;
}
uint8_t r;
uint8_t g;
uint8_t b;
dvxGetColor(sAc, (ColorIdE)sel, &r, &g, &b);
wgtSliderSetValue(sRedSldr, r);
wgtSliderSetValue(sGreenSldr, g);
wgtSliderSetValue(sBlueSldr, b);
static char rBuf[8];
static char gBuf[8];
static char bBuf[8];
snprintf(rBuf, sizeof(rBuf), "%d", r);
snprintf(gBuf, sizeof(gBuf), "%d", g);
snprintf(bBuf, sizeof(bBuf), "%d", b);
wgtSetText(sRedLbl, rBuf);
wgtSetText(sGreenLbl, gBuf);
wgtSetText(sBlueLbl, bBuf);
updateSwatch();
}
// ============================================================
// updateDblClickLabel
// ============================================================
static void updateDblClickLabel(void) {
static char buf[32];
snprintf(buf, sizeof(buf), "%ld ms", (long)wgtSliderGetValue(sDblClickSldr));
wgtSetText(sDblClickLbl, buf);
}
// ============================================================
// updateSwatch
// ============================================================
static void updateSwatch(void) {
if (!sColorSwatch) {
return;
}
uint8_t r = (uint8_t)wgtSliderGetValue(sRedSldr);
uint8_t g = (uint8_t)wgtSliderGetValue(sGreenSldr);
uint8_t b = (uint8_t)wgtSliderGetValue(sBlueSldr);
uint32_t color = packColor(&sAc->display, r, g, b);
wgtCanvasClear(sColorSwatch, color);
}
// ============================================================
// appMain
// ============================================================
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
sAc = ctx->shellCtx;
int32_t winX = (sAc->display.width - CP_WIN_W) / 2;
int32_t winY = (sAc->display.height - CP_WIN_H) / 2;
sWin = dvxCreateWindow(sAc, "Control Panel", winX, winY, CP_WIN_W, CP_WIN_H, true);
if (!sWin) {
return -1;
}
sWin->onClose = onClose;
saveSnapshot();
WidgetT *root = wgtInitWindow(sAc, sWin);
// Tab control fills the window
WidgetT *tabs = wgtTabControl(root);
tabs->weight = 100;
WidgetT *mouseTab = wgtTabPage(tabs, "Mouse");
WidgetT *colorsTab = wgtTabPage(tabs, "Colors");
WidgetT *desktopTab = wgtTabPage(tabs, "Desktop");
WidgetT *videoTab = wgtTabPage(tabs, "Video");
buildMouseTab(mouseTab);
buildColorsTab(colorsTab);
buildDesktopTab(desktopTab);
buildVideoTab(videoTab);
// OK / Cancel buttons at bottom
WidgetT *btnRow = wgtHBox(root);
btnRow->align = AlignEndE;
btnRow->spacing = wgtPixels(8);
WidgetT *okBtn = wgtButton(btnRow, "OK");
okBtn->onClick = onOk;
okBtn->prefW = wgtPixels(80);
WidgetT *cancelBtn = wgtButton(btnRow, "Cancel");
cancelBtn->onClick = onCancel;
cancelBtn->prefW = wgtPixels(80);
dvxFitWindow(sAc, sWin);
return 0;
}