Skip to content

Commit 325d846

Browse files
committed
feat(tray_animation): added capslock
1 parent 939c022 commit 325d846

File tree

6 files changed

+218
-14
lines changed

6 files changed

+218
-14
lines changed

include/tray/tray_animation_loader.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ typedef enum {
2323
ANIM_SOURCE_UNKNOWN = 0,
2424
ANIM_SOURCE_LOGO,
2525
ANIM_SOURCE_PERCENT,
26+
ANIM_SOURCE_CAPSLOCK,
2627
ANIM_SOURCE_GIF,
2728
ANIM_SOURCE_WEBP,
2829
ANIM_SOURCE_STATIC,

include/tray/tray_animation_percent.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,22 @@ COLORREF GetPercentIconBgColor(void);
5050
*/
5151
HICON CreatePercentIcon16(int percent);
5252

53+
/**
54+
* @brief Create Caps Lock indicator icon
55+
* @param capsOn TRUE if Caps Lock is on, FALSE otherwise
56+
* @return HICON or NULL on failure
57+
*
58+
* @details
59+
* Renders "A" when Caps Lock is on, "a" when off.
60+
* Uses same color scheme as percent icons.
61+
*/
62+
HICON CreateCapsLockIcon(BOOL capsOn);
63+
64+
/**
65+
* @brief Check current Caps Lock state
66+
* @return TRUE if Caps Lock is on
67+
*/
68+
BOOL IsCapsLockOn(void);
69+
5370
#endif /* TRAY_ANIMATION_PERCENT_H */
5471

resource/resource.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@
371371
#define CLOCK_IDM_ANIMATIONS_USE_CPU 2203 /**< Use CPU animation */
372372
#define CLOCK_IDM_ANIMATIONS_USE_MEM 2204 /**< Use memory animation */
373373
#define CLOCK_IDM_ANIMATIONS_USE_BATTERY 2205 /**< Use battery percent icon */
374+
#define CLOCK_IDM_ANIMATIONS_USE_CAPSLOCK 2207 /**< Use Caps Lock indicator */
374375
#define CLOCK_IDM_ANIMATIONS_USE_NONE 2206 /**< Use transparent/hidden icon */
375376
#define CLOCK_IDM_ANIM_SPEED_MEMORY 2210 /**< Memory-based animation speed */
376377
#define CLOCK_IDM_ANIM_SPEED_CPU 2211 /**< CPU-based animation speed */

src/tray/tray_animation_core.c

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,25 @@ static void UpdateTrayIconToCurrentFrame(void) {
247247
return;
248248
}
249249

250+
/* Handle Caps Lock indicator */
251+
if (currentAnim->sourceType == ANIM_SOURCE_CAPSLOCK) {
252+
BOOL capsOn = IsCapsLockOn();
253+
254+
HICON hIcon = CreateCapsLockIcon(capsOn);
255+
if (hIcon) {
256+
NOTIFYICONDATAW nid = {0};
257+
nid.cbSize = sizeof(nid);
258+
nid.hWnd = g_trayHwnd;
259+
nid.uID = CLOCK_ID_TRAY_APP_ICON;
260+
nid.uFlags = NIF_ICON;
261+
nid.hIcon = hIcon;
262+
Shell_NotifyIconW(NIM_MODIFY, &nid);
263+
DestroyIcon(hIcon);
264+
RecordSuccessfulUpdate();
265+
}
266+
return;
267+
}
268+
250269
if (currentAnim->count <= 0) {
251270
if (g_isPreviewActive) {
252271
g_isPreviewActive = FALSE;
@@ -469,7 +488,9 @@ BOOL SetCurrentAnimationName(const char* name) {
469488

470489
/* Seamless preview promotion */
471490
if (g_isPreviewActive && g_previewAnimationName[0] != '\0' &&
472-
_stricmp(g_previewAnimationName, name) == 0 && g_previewAnimation.count > 0) {
491+
_stricmp(g_previewAnimationName, name) == 0 &&
492+
(g_previewAnimation.count > 0 || g_previewAnimation.sourceType == ANIM_SOURCE_PERCENT ||
493+
g_previewAnimation.sourceType == ANIM_SOURCE_CAPSLOCK)) {
473494

474495
if (g_criticalSectionInitialized) {
475496
EnterCriticalSection(&g_animCriticalSection);
@@ -639,9 +660,9 @@ static DWORD WINAPI AsyncLoadPreviewThread(LPVOID param) {
639660
g_previewIndex = 0;
640661
g_frameRateCtrl.framePosition = 0.0;
641662

642-
/* For percent icons (count=0), __none__ (transparent), or regular animations (count>0), activate preview */
663+
/* For percent icons, capslock icons (count=0), __none__ (transparent), or regular animations (count>0), activate preview */
643664
if (tempAnim.count > 0 || tempAnim.sourceType == ANIM_SOURCE_PERCENT ||
644-
_stricmp(name, "__none__") == 0) {
665+
tempAnim.sourceType == ANIM_SOURCE_CAPSLOCK || _stricmp(name, "__none__") == 0) {
645666
strncpy(g_previewAnimationName, name, sizeof(g_previewAnimationName) - 1);
646667
g_previewAnimationName[sizeof(g_previewAnimationName) - 1] = '\0';
647668
g_isPreviewActive = TRUE;
@@ -903,22 +924,31 @@ void TrayAnimation_ClearCurrentName(void) {
903924
void TrayAnimation_UpdatePercentIconIfNeeded(void) {
904925
if (!g_trayHwnd || !IsWindow(g_trayHwnd)) return;
905926
if (!g_animationName[0]) return;
927+
if (g_isPreviewActive) return;
906928

907-
/* Only update if it's a builtin percent type (e.g. __cpu__, __mem__, __battery__) */
908929
const BuiltinAnimDef* def = GetBuiltinAnimDef(g_animationName);
909-
if (!def || def->type != ANIM_SOURCE_PERCENT) return;
930+
if (!def) return;
910931

911-
if (g_isPreviewActive) return;
932+
HICON hIcon = NULL;
912933

913-
int p = 0;
914-
if (def->getValue) {
915-
p = def->getValue();
934+
/* Handle percent type (CPU, Memory, Battery) */
935+
if (def->type == ANIM_SOURCE_PERCENT) {
936+
int p = 0;
937+
if (def->getValue) {
938+
p = def->getValue();
939+
}
940+
if (p < 0) p = 0;
941+
if (p > 100) p = 100;
942+
hIcon = CreatePercentIcon16(p);
943+
}
944+
/* Handle Caps Lock indicator */
945+
else if (def->type == ANIM_SOURCE_CAPSLOCK) {
946+
hIcon = CreateCapsLockIcon(IsCapsLockOn());
947+
}
948+
else {
949+
return;
916950
}
917951

918-
if (p < 0) p = 0;
919-
if (p > 100) p = 100;
920-
921-
HICON hIcon = CreatePercentIcon16(p);
922952
if (!hIcon) return;
923953

924954
NOTIFYICONDATAW nid = {0};

src/tray/tray_animation_loader.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ static const BuiltinAnimDef g_builtinAnims[] = {
5555
{ "__cpu__", CLOCK_IDM_ANIMATIONS_USE_CPU, L"CPU Percent", ANIM_SOURCE_PERCENT, GetCpuValue },
5656
{ "__mem__", CLOCK_IDM_ANIMATIONS_USE_MEM, L"Memory Percent", ANIM_SOURCE_PERCENT, GetMemValue },
5757
{ "__battery__", CLOCK_IDM_ANIMATIONS_USE_BATTERY, L"Battery Percent", ANIM_SOURCE_PERCENT, GetBatteryValue },
58+
{ "__capslock__", CLOCK_IDM_ANIMATIONS_USE_CAPSLOCK, L"Caps Lock", ANIM_SOURCE_CAPSLOCK, NULL },
5859
{ "__none__", CLOCK_IDM_ANIMATIONS_USE_NONE, L"None", ANIM_SOURCE_UNKNOWN, NULL }
5960
};
6061

@@ -300,7 +301,7 @@ BOOL IsValidAnimationSource(const char* name) {
300301

301302
AnimationSourceType type = DetectAnimationSourceType(name);
302303

303-
if (type == ANIM_SOURCE_LOGO || type == ANIM_SOURCE_PERCENT) {
304+
if (type == ANIM_SOURCE_LOGO || type == ANIM_SOURCE_PERCENT || type == ANIM_SOURCE_CAPSLOCK) {
304305
return TRUE;
305306
}
306307

@@ -380,6 +381,13 @@ BOOL LoadAnimationByName(const char* name, LoadedAnimation* anim,
380381
return TRUE;
381382
}
382383

384+
if (type == ANIM_SOURCE_CAPSLOCK) {
385+
/* Caps Lock icons are generated dynamically, not pre-loaded */
386+
anim->count = 0;
387+
anim->isAnimated = FALSE;
388+
return TRUE;
389+
}
390+
383391
/* Handle __none__ (transparent icon) - no frames needed */
384392
if (_stricmp(name, "__none__") == 0) {
385393
anim->count = 0;

src/tray/tray_animation_percent.c

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,150 @@ HICON CreatePercentIcon16(int percent) {
213213
return hIcon;
214214
}
215215

216+
/**
217+
* @brief Check current Caps Lock state
218+
*/
219+
BOOL IsCapsLockOn(void) {
220+
return (GetKeyState(VK_CAPITAL) & 0x0001) != 0;
221+
}
222+
223+
/**
224+
* @brief Create Caps Lock indicator icon
225+
*/
226+
HICON CreateCapsLockIcon(BOOL capsOn) {
227+
int cx = GetSystemMetrics(SM_CXSMICON);
228+
int cy = GetSystemMetrics(SM_CYSMICON);
229+
if (cx <= 0) cx = 16;
230+
if (cy <= 0) cy = 16;
231+
232+
/* Determine colors: use theme-based or user-configured */
233+
BOOL useTransparentBg = (g_percentBgColor == TRANSPARENT_BG_AUTO);
234+
COLORREF textColor = useTransparentBg ? GetThemeTextColor() : g_percentTextColor;
235+
COLORREF bgColor = g_percentBgColor;
236+
237+
/* Create DIB section for color bitmap */
238+
BITMAPINFO bi;
239+
ZeroMemory(&bi, sizeof(bi));
240+
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
241+
bi.bmiHeader.biWidth = cx;
242+
bi.bmiHeader.biHeight = -cy;
243+
bi.bmiHeader.biPlanes = 1;
244+
bi.bmiHeader.biBitCount = 32;
245+
bi.bmiHeader.biCompression = BI_RGB;
246+
247+
VOID* pvBits = NULL;
248+
HBITMAP hbmColor = CreateDIBSection(NULL, &bi, DIB_RGB_COLORS, &pvBits, NULL, 0);
249+
if (!hbmColor || !pvBits) {
250+
if (hbmColor) DeleteObject(hbmColor);
251+
return NULL;
252+
}
253+
254+
/* Draw on color bitmap */
255+
HDC hdc = GetDC(NULL);
256+
HDC mem = CreateCompatibleDC(hdc);
257+
HGDIOBJ old = SelectObject(mem, hbmColor);
258+
259+
if (useTransparentBg) {
260+
/* Transparent background: fill with transparent pixels (alpha=0) */
261+
DWORD* pixels = (DWORD*)pvBits;
262+
for (int i = 0; i < cx * cy; i++) {
263+
pixels[i] = 0x00000000;
264+
}
265+
} else {
266+
/* Solid background: use configured color */
267+
RECT rc = {0, 0, cx, cy};
268+
HBRUSH bk = CreateSolidBrush(bgColor);
269+
FillRect(mem, &rc, bk);
270+
DeleteObject(bk);
271+
}
272+
273+
/* Setup text rendering */
274+
SetBkMode(mem, TRANSPARENT);
275+
SetTextColor(mem, textColor);
276+
277+
/* Display "A" or "a" based on Caps Lock state */
278+
const wchar_t* txt = capsOn ? L"A" : L"a";
279+
280+
/* Font size - -13 is the standard system icon font size */
281+
int fontSize = -13;
282+
283+
/* Use FW_NORMAL (400) to match system UI look, ANTIALIASED_QUALITY for smooth blending */
284+
HFONT hFont = CreateFontW(fontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
285+
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
286+
ANTIALIASED_QUALITY, VARIABLE_PITCH | FF_SWISS, L"Segoe UI");
287+
HFONT oldf = hFont ? (HFONT)SelectObject(mem, hFont) : NULL;
288+
289+
/* Center text */
290+
SIZE sz = {0};
291+
GetTextExtentPoint32W(mem, txt, 1, &sz);
292+
int x = (cx - sz.cx) / 2;
293+
int y = (cy - sz.cy) / 2;
294+
295+
TextOutW(mem, x, y, txt, 1);
296+
297+
/* Cleanup font */
298+
if (oldf) SelectObject(mem, oldf);
299+
if (hFont) DeleteObject(hFont);
300+
301+
SelectObject(mem, old);
302+
303+
/* Create mask bitmap */
304+
HBITMAP hbmMask = CreateBitmap(cx, cy, 1, 1, NULL);
305+
if (!hbmMask) {
306+
ReleaseDC(NULL, hdc);
307+
DeleteDC(mem);
308+
DeleteObject(hbmColor);
309+
return NULL;
310+
}
311+
312+
HDC memMask = CreateCompatibleDC(hdc);
313+
HGDIOBJ oldMask = SelectObject(memMask, hbmMask);
314+
315+
if (useTransparentBg) {
316+
/* For transparent background: mask = white for bg, black for text */
317+
RECT rc = {0, 0, cx, cy};
318+
HBRUSH whiteBrush = CreateSolidBrush(RGB(255, 255, 255));
319+
FillRect(memMask, &rc, whiteBrush);
320+
DeleteObject(whiteBrush);
321+
322+
/* Draw text in black on mask */
323+
SetBkMode(memMask, TRANSPARENT);
324+
SetTextColor(memMask, RGB(0, 0, 0));
325+
326+
HFONT hMaskFont = CreateFontW(fontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
327+
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
328+
ANTIALIASED_QUALITY, VARIABLE_PITCH | FF_SWISS, L"Segoe UI");
329+
HFONT oldMaskFont = hMaskFont ? (HFONT)SelectObject(memMask, hMaskFont) : NULL;
330+
331+
TextOutW(memMask, x, y, txt, 1);
332+
333+
if (oldMaskFont) SelectObject(memMask, oldMaskFont);
334+
if (hMaskFont) DeleteObject(hMaskFont);
335+
} else {
336+
/* For solid background: entire icon is opaque */
337+
RECT rc = {0, 0, cx, cy};
338+
HBRUSH blackBrush = CreateSolidBrush(RGB(0, 0, 0));
339+
FillRect(memMask, &rc, blackBrush);
340+
DeleteObject(blackBrush);
341+
}
342+
343+
SelectObject(memMask, oldMask);
344+
DeleteDC(memMask);
345+
ReleaseDC(NULL, hdc);
346+
DeleteDC(mem);
347+
348+
/* Create icon */
349+
ICONINFO ii;
350+
ZeroMemory(&ii, sizeof(ii));
351+
ii.fIcon = TRUE;
352+
ii.hbmColor = hbmColor;
353+
ii.hbmMask = hbmMask;
354+
355+
HICON hIcon = CreateIconIndirect(&ii);
356+
357+
DeleteObject(hbmMask);
358+
DeleteObject(hbmColor);
359+
360+
return hIcon;
361+
}
362+

0 commit comments

Comments
 (0)