-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathIconManager.cs
More file actions
140 lines (121 loc) · 4.56 KB
/
IconManager.cs
File metadata and controls
140 lines (121 loc) · 4.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using System.Reflection;
namespace CapsNumTray;
/// <summary>
/// Loads and caches icon handles. Tracks ownership for cleanup.
/// 3-stage fallback: embedded resource → .ico file on disk → system icon.
/// </summary>
internal sealed class IconManager : IDisposable
{
private readonly int _iconSize;
private readonly HashSet<nint> _ownedHandles = new();
private bool _disposed;
public nint CapsOn { get; }
public nint CapsOff { get; private set; }
public nint NumOn { get; }
public nint NumOff { get; private set; }
public nint ScrollOn { get; }
public nint ScrollOff { get; private set; }
public IconManager(nint windowHandle, bool lightTheme)
{
_iconSize = GetDpiAwareIconSize(windowHandle);
CapsOn = LoadIcon("CapsLockOn", 32516);
CapsOff = LoadIcon(lightTheme ? "CapsLockOff_Light" : "CapsLockOff", 32515);
NumOn = LoadIcon("NumLockOn", 32516);
NumOff = LoadIcon(lightTheme ? "NumLockOff_Light" : "NumLockOff", 32515);
ScrollOn = LoadIcon("ScrollLockOn", 32516);
ScrollOff = LoadIcon(lightTheme ? "ScrollLockOff_Light" : "ScrollLockOff", 32515);
}
// Reload the theme-dependent OFF icons when the system theme flips
// between light and dark at runtime. ON icons are theme-independent.
public void ReloadForTheme(bool lightTheme)
{
DisposeIfOwned(CapsOff);
DisposeIfOwned(NumOff);
DisposeIfOwned(ScrollOff);
CapsOff = LoadIcon(lightTheme ? "CapsLockOff_Light" : "CapsLockOff", 32515);
NumOff = LoadIcon(lightTheme ? "NumLockOff_Light" : "NumLockOff", 32515);
ScrollOff = LoadIcon(lightTheme ? "ScrollLockOff_Light" : "ScrollLockOff", 32515);
}
private void DisposeIfOwned(nint h)
{
if (_ownedHandles.Remove(h))
NativeMethods.DestroyIcon(h);
}
private static int GetDpiAwareIconSize(nint windowHandle)
{
uint dpi = NativeMethods.GetDpiForWindow(windowHandle);
if (dpi == 0)
dpi = NativeMethods.GetDpiForSystem();
if (dpi == 0)
dpi = 96;
return (int)Math.Round(16.0 * dpi / 96.0);
}
/// <summary>
/// 3-stage icon loading:
/// 1. Embedded resource (works in published exe)
/// 2. .ico file on disk (dev/source mode)
/// 3. System fallback icon
/// </summary>
private nint LoadIcon(string name, int fallbackOrdinal)
{
// Stage 1: Embedded resource
nint h = LoadFromEmbeddedResource(name);
if (h != 0) return h;
// Stage 2: File on disk
h = LoadFromFile(name);
if (h != 0) return h;
// Stage 3: System icon (shared handle — do NOT track for destruction)
return NativeMethods.LoadIcon(0, (nint)fallbackOrdinal);
}
private nint LoadFromEmbeddedResource(string name)
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream(name + ".ico");
if (stream == null) return 0;
// Write to a temp file, then LoadImage for correct sizing.
// Use random filename to prevent TOCTOU race (predictable path = plantable).
string tempPath = Path.Combine(Path.GetTempPath(), $"CapsNumTray_{Guid.NewGuid():N}.ico");
try
{
using (var fs = File.Create(tempPath))
stream.CopyTo(fs);
nint h = NativeMethods.LoadImage(0, tempPath, NativeMethods.IMAGE_ICON,
_iconSize, _iconSize, NativeMethods.LR_LOADFROMFILE);
if (h != 0)
_ownedHandles.Add(h);
return h;
}
catch
{
return 0;
}
finally
{
try { File.Delete(tempPath); } catch { }
}
}
private nint LoadFromFile(string name)
{
string? exeDir = Path.GetDirectoryName(Environment.ProcessPath);
if (exeDir == null) return 0;
string path = Path.Combine(exeDir, "icons", name + ".ico");
// No File.Exists guard — LoadImage returns 0 on missing/inaccessible files,
// and adding a check would widen the TOCTOU window for no benefit.
nint h = NativeMethods.LoadImage(0, path, NativeMethods.IMAGE_ICON,
_iconSize, _iconSize, NativeMethods.LR_LOADFROMFILE);
if (h != 0)
_ownedHandles.Add(h);
return h;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
foreach (nint h in _ownedHandles)
{
if (h != 0)
NativeMethods.DestroyIcon(h);
}
_ownedHandles.Clear();
}
}