From b258630e07791494f3557da00836ab9c8fff1c02 Mon Sep 17 00:00:00 2001 From: Trevor Hall Date: Tue, 9 Dec 2025 21:43:34 -0500 Subject: [PATCH] Add project files. --- PrivacyGlass.sln | 25 +++ PrivacyGlass/AcrylicOverlay.cs | 124 ++++++++++++ PrivacyGlass/App.config | 6 + PrivacyGlass/BlurHelper.cs | 41 ++++ PrivacyGlass/BlurOverlay.cs | 143 +++++++++++++ PrivacyGlass/GaussianBlur.cs | 78 ++++++++ PrivacyGlass/HotkeyFilter.cs | 23 +++ PrivacyGlass/MagnifyOverlay.cs | 189 ++++++++++++++++++ PrivacyGlass/Native.cs | 158 +++++++++++++++ PrivacyGlass/OverlayWindow.cs | 149 ++++++++++++++ PrivacyGlass/PrivacyGlass.csproj | 96 +++++++++ PrivacyGlass/Program.cs | 24 +++ PrivacyGlass/Properties/AssemblyInfo.cs | 33 +++ PrivacyGlass/Properties/Resources.Designer.cs | 71 +++++++ PrivacyGlass/Properties/Resources.resx | 117 +++++++++++ PrivacyGlass/Properties/Settings.Designer.cs | 30 +++ PrivacyGlass/Properties/Settings.settings | 7 + PrivacyGlass/Resources/privacyglass.ico | Bin 0 -> 193 bytes PrivacyGlass/TrayApp.cs | 132 ++++++++++++ 19 files changed, 1446 insertions(+) create mode 100644 PrivacyGlass.sln create mode 100644 PrivacyGlass/AcrylicOverlay.cs create mode 100644 PrivacyGlass/App.config create mode 100644 PrivacyGlass/BlurHelper.cs create mode 100644 PrivacyGlass/BlurOverlay.cs create mode 100644 PrivacyGlass/GaussianBlur.cs create mode 100644 PrivacyGlass/HotkeyFilter.cs create mode 100644 PrivacyGlass/MagnifyOverlay.cs create mode 100644 PrivacyGlass/Native.cs create mode 100644 PrivacyGlass/OverlayWindow.cs create mode 100644 PrivacyGlass/PrivacyGlass.csproj create mode 100644 PrivacyGlass/Program.cs create mode 100644 PrivacyGlass/Properties/AssemblyInfo.cs create mode 100644 PrivacyGlass/Properties/Resources.Designer.cs create mode 100644 PrivacyGlass/Properties/Resources.resx create mode 100644 PrivacyGlass/Properties/Settings.Designer.cs create mode 100644 PrivacyGlass/Properties/Settings.settings create mode 100644 PrivacyGlass/Resources/privacyglass.ico create mode 100644 PrivacyGlass/TrayApp.cs diff --git a/PrivacyGlass.sln b/PrivacyGlass.sln new file mode 100644 index 0000000..afd7fdb --- /dev/null +++ b/PrivacyGlass.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36429.23 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrivacyGlass", "PrivacyGlass\PrivacyGlass.csproj", "{71C41874-9B78-4484-82B9-9D5384C18E1D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {71C41874-9B78-4484-82B9-9D5384C18E1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71C41874-9B78-4484-82B9-9D5384C18E1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71C41874-9B78-4484-82B9-9D5384C18E1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71C41874-9B78-4484-82B9-9D5384C18E1D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CF678296-C351-43AB-A05F-3580EC7BC900} + EndGlobalSection +EndGlobal diff --git a/PrivacyGlass/AcrylicOverlay.cs b/PrivacyGlass/AcrylicOverlay.cs new file mode 100644 index 0000000..81adda5 --- /dev/null +++ b/PrivacyGlass/AcrylicOverlay.cs @@ -0,0 +1,124 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace PrivacyGlass +{ + public class AcrylicOverlay : Form + { + private int _tintOpacity = 70; + private Color _tintColor = Color.Green; + private bool _isEnabled = false; + + public int TintOpacity + { + get { return _tintOpacity; } + set { _tintOpacity = value; } + } + + public Color TintColor + { + get { return _tintColor; } + set { _tintColor = value; } + } + + public AcrylicOverlay(Rectangle bounds) + { + FormBorderStyle = FormBorderStyle.None; + StartPosition = FormStartPosition.Manual; + Bounds = bounds; + ShowInTaskbar = false; + TopMost = true; + } + + public void Toggle() + { + if (_isEnabled) + { + _isEnabled = false; + Hide(); + } + else + { + _isEnabled = true; + Show(); + EnableAcrylic(); + } + } + + protected override bool ShowWithoutActivation + { + get { return true; } + } + + protected override void OnPaint(PaintEventArgs e) + { + using (SolidBrush brush = new SolidBrush(Color.FromArgb(_tintOpacity, _tintColor))) + { + e.Graphics.FillRectangle(brush, this.ClientRectangle); + } + } + + private void EnableAcrylic() + { + ACCENT_POLICY accent = new ACCENT_POLICY(); + accent.AccentState = (int)AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND; + accent.AccentFlags = 2; // simplest, most compatible + accent.Color = (_tintOpacity << 24) | + (_tintColor.R << 16) | + (_tintColor.G << 8) | + _tintColor.B; + + int size = Marshal.SizeOf(typeof(ACCENT_POLICY)); + IntPtr accentPtr = Marshal.AllocHGlobal(size); + Marshal.StructureToPtr(accent, accentPtr, false); + + WINDOWCOMPOSITIONATTRIBDATA data = new WINDOWCOMPOSITIONATTRIBDATA(); + data.Attribute = (int)WindowCompositionAttribute.WCA_ACCENT_POLICY; + data.Data = accentPtr; + data.SizeOfData = size; + + SetWindowCompositionAttribute(this.Handle, ref data); + Marshal.FreeHGlobal(accentPtr); + } + + // ---------------------------------------------------- + // Native + // ---------------------------------------------------- + + private enum AccentState + { + ACCENT_DISABLED = 0, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_ENABLE_ACRYLICBLURBEHIND = 4 + } + + private enum WindowCompositionAttribute + { + WCA_ACCENT_POLICY = 19 + } + + [StructLayout(LayoutKind.Sequential)] + private struct ACCENT_POLICY + { + public int AccentState; + public int AccentFlags; + public int Color; + public int AnimationId; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WINDOWCOMPOSITIONATTRIBDATA + { + public int Attribute; + public IntPtr Data; + public int SizeOfData; + } + + [DllImport("user32.dll")] + private static extern int SetWindowCompositionAttribute( + IntPtr hwnd, + ref WINDOWCOMPOSITIONATTRIBDATA data); + } +} diff --git a/PrivacyGlass/App.config b/PrivacyGlass/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/PrivacyGlass/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/PrivacyGlass/BlurHelper.cs b/PrivacyGlass/BlurHelper.cs new file mode 100644 index 0000000..3580033 --- /dev/null +++ b/PrivacyGlass/BlurHelper.cs @@ -0,0 +1,41 @@ + +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace PrivacyGlass +{ + + public static class BlurHelper + { + // factor 4–12 is good; higher = more blurry + public static Bitmap DownscaleBlur(Bitmap source, int factor = 8) + { + if (factor < 2) factor = 2; + + int w = source.Width / factor; + int h = source.Height / factor; + if (w < 1) w = 1; + if (h < 1) h = 1; + + // downscale + var small = new Bitmap(w, h); + using (var g = Graphics.FromImage(small)) + { + g.InterpolationMode = InterpolationMode.HighQualityBilinear; + g.DrawImage(source, new Rectangle(0, 0, w, h)); + } + + // upscale back + var blurred = new Bitmap(source.Width, source.Height); + using (var g = Graphics.FromImage(blurred)) + { + g.InterpolationMode = InterpolationMode.HighQualityBilinear; + g.DrawImage(small, new Rectangle(0, 0, blurred.Width, blurred.Height)); + } + + small.Dispose(); + return blurred; + } + } + +} diff --git a/PrivacyGlass/BlurOverlay.cs b/PrivacyGlass/BlurOverlay.cs new file mode 100644 index 0000000..527be96 --- /dev/null +++ b/PrivacyGlass/BlurOverlay.cs @@ -0,0 +1,143 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace PrivacyGlass +{ + public class BlurOverlay : Form + { + public int TintOpacity = 128; // 0–255 + public Color TintColor = Color.Black; // tint over the blur + private bool _enabled = false; + + public BlurOverlay(Rectangle bounds) + { + FormBorderStyle = FormBorderStyle.None; + StartPosition = FormStartPosition.Manual; + ShowInTaskbar = false; + TopMost = true; + Bounds = bounds; + + // Important: background color = transparency key + // so GDI background is not drawn, blur shows through. + BackColor = Color.Black; + TransparencyKey = Color.Black; + } + + protected override CreateParams CreateParams + { + get + { + var cp = base.CreateParams; + cp.Style = unchecked((int)0x80000000); // WS_POPUP + return cp; + } + } + + protected override bool ShowWithoutActivation => true; + + protected override void WndProc(ref Message m) + { + const int WM_NCHITTEST = 0x84; + const int HTTRANSPARENT = -1; + + if (m.Msg == WM_NCHITTEST) + { + // Click-through + m.Result = (IntPtr)HTTRANSPARENT; + return; + } + + base.WndProc(ref m); + } + + public void Toggle() + { + if (_enabled) + { + _enabled = false; + Hide(); + } + else + { + _enabled = true; + Show(); + BeginInvoke((Action)ApplyBlur); + } + } + + private void ApplyBlur() + { + // ACCENT_ENABLE_BLURBEHIND on Win10/11 + AccentPolicy accent = new AccentPolicy(); + accent.AccentState = AccentState.ACCENT_ENABLE_BLURBEHIND; + + // Optional tint over blur (ARGB, but note it's BGR in low 24 bits) + int a = TintOpacity & 0xFF; + int color = + (a << 24) | + (TintColor.B << 16) | + (TintColor.G << 8) | + TintColor.R; + + accent.GradientColor = color; + + int size = Marshal.SizeOf(accent); + IntPtr ptr = Marshal.AllocHGlobal(size); + try + { + Marshal.StructureToPtr(accent, ptr, false); + + WindowCompositionAttributeData data = new WindowCompositionAttributeData(); + data.Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY; + data.SizeOfData = size; + data.Data = ptr; + + SetWindowCompositionAttribute(this.Handle, ref data); + } + finally + { + Marshal.FreeHGlobal(ptr); + } + } + + // -------- interop stuff below -------- + + private enum AccentState + { + ACCENT_DISABLED = 0, + ACCENT_ENABLE_GRADIENT = 1, + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_ENABLE_ACRYLICBLURBEHIND = 4 + } + + [StructLayout(LayoutKind.Sequential)] + private struct AccentPolicy + { + public AccentState AccentState; + public int AccentFlags; + public int GradientColor; + public int AnimationId; + } + + private enum WindowCompositionAttribute + { + WCA_ACCENT_POLICY = 19 + } + + [StructLayout(LayoutKind.Sequential)] + private struct WindowCompositionAttributeData + { + public WindowCompositionAttribute Attribute; + public IntPtr Data; + public int SizeOfData; + } + + [DllImport("user32.dll")] + private static extern int SetWindowCompositionAttribute( + IntPtr hwnd, + ref WindowCompositionAttributeData data); + } +} diff --git a/PrivacyGlass/GaussianBlur.cs b/PrivacyGlass/GaussianBlur.cs new file mode 100644 index 0000000..d777f4a --- /dev/null +++ b/PrivacyGlass/GaussianBlur.cs @@ -0,0 +1,78 @@ + +using System; +using System.Drawing; +using System.Drawing.Imaging; + +namespace PrivacyGlass +{ + + public static class GaussianBlur + { + // 5x5 gaussian kernel (sigma ~1.0) + private static readonly double[,] kernel = + { + { 1, 4, 7, 4, 1 }, + { 4, 16, 26, 16, 4 }, + { 7, 26, 41, 26, 7 }, + { 4, 16, 26, 16, 4 }, + { 1, 4, 7, 4, 1 } + }; + + private const double kernelSum = 273; + + public static unsafe Bitmap Blur(Bitmap image) + { + int w = image.Width; + int h = image.Height; + Bitmap blurred = new Bitmap(w, h, PixelFormat.Format32bppArgb); + + BitmapData srcData = + image.LockBits(new Rectangle(0, 0, w, h), + ImageLockMode.ReadOnly, + PixelFormat.Format32bppArgb); + + BitmapData dstData = + blurred.LockBits(new Rectangle(0, 0, w, h), + ImageLockMode.WriteOnly, + PixelFormat.Format32bppArgb); + + int stride = srcData.Stride; + + byte* src = (byte*)srcData.Scan0; + byte* dst = (byte*)dstData.Scan0; + + for (int y = 2; y < h - 2; y++) + { + for (int x = 2; x < w - 2; x++) + { + double b = 0, g = 0, r = 0; + + // apply kernel + for (int ky = -2; ky <= 2; ky++) + { + for (int kx = -2; kx <= 2; kx++) + { + byte* p = src + ((y + ky) * stride) + ((x + kx) * 4); + double wgt = kernel[ky + 2, kx + 2]; + + b += p[0] * wgt; + g += p[1] * wgt; + r += p[2] * wgt; + } + } + + byte* o = dst + (y * stride) + (x * 4); + o[0] = (byte)(b / kernelSum); + o[1] = (byte)(g / kernelSum); + o[2] = (byte)(r / kernelSum); + o[3] = 255; + } + } + + image.UnlockBits(srcData); + blurred.UnlockBits(dstData); + return blurred; + } + } + +} diff --git a/PrivacyGlass/HotkeyFilter.cs b/PrivacyGlass/HotkeyFilter.cs new file mode 100644 index 0000000..5299f62 --- /dev/null +++ b/PrivacyGlass/HotkeyFilter.cs @@ -0,0 +1,23 @@ + +using System; +using System.Windows.Forms; + +namespace PrivacyGlass +{ + public class HotkeyFilter : IMessageFilter + { + private readonly Action onHotkey; + public HotkeyFilter(Action a) => onHotkey = a; + + public bool PreFilterMessage(ref Message m) + { + if (m.Msg == 0x0312) // WM_HOTKEY + { + onHotkey(); + return true; + } + return false; + } + } + +} diff --git a/PrivacyGlass/MagnifyOverlay.cs b/PrivacyGlass/MagnifyOverlay.cs new file mode 100644 index 0000000..5749c9d --- /dev/null +++ b/PrivacyGlass/MagnifyOverlay.cs @@ -0,0 +1,189 @@ + +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace PrivacyGlass +{ + + namespace PrivacyGlass + { + public class MagnifyOverlay : Form + { + private static bool _magInitialized; + private readonly Rectangle _screenBounds; + private IntPtr _magWindow = IntPtr.Zero; + + public bool Blackout { get; set; } = false; // set true if you want pure black instead + public int Darken = 80; // optional extra dark tint 0–100 + public double blurScale = 0.5; + + public MagnifyOverlay(Rectangle bounds) + { + _screenBounds = bounds; + + FormBorderStyle = FormBorderStyle.None; + StartPosition = FormStartPosition.Manual; + Bounds = bounds; + ShowInTaskbar = false; + TopMost = true; + DoubleBuffered = true; + BackColor = Color.Black; + Opacity = 1.0; + Visible = false; + } + private void ApplyBlurTransform() + { + // scale down slightly -> blur when drawn full size + float scale = 0.1f; // 0.7 is lighter blur, 0.3 is heavy blur + + var m = new Native.MAGTRANSFORM + { + v = new float[9] + }; + + m.v[0] = scale; // scale X + m.v[4] = scale; // scale Y + m.v[8] = 1.0f; // w + + Native.MagSetWindowTransform(_magWindow, ref m); + } + private void RefreshSource() + { + if (_magWindow == IntPtr.Zero) + return; + + // full screen size + int fullW = _screenBounds.Width; + int fullH = _screenBounds.Height; + + // reduced sampling size + int sampleW = (int)(fullW * blurScale); + int sampleH = (int)(fullH * blurScale); + + // center the sampling rect + int left = _screenBounds.Left + (fullW - sampleW) / 2; + int top = _screenBounds.Top + (fullH - sampleH) / 2; + int right = left + sampleW; + int bottom = top + sampleH; + + var srcRect = new Native.RECT + { + left = left, + top = top, + right = right, + bottom = bottom + }; + + Native.MagSetWindowSource(_magWindow, srcRect); + } + + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + + if (!_magInitialized) + { + // Magnification API: initialize once per process + _magInitialized = Native.MagInitialize(); + } + + // Make the host window layered & topmost (we keep it opaque for now) + int ex = (int)Native.GetWindowLong(Handle, Native.GWL_EXSTYLE); + ex |= Native.WS_EX_TOPMOST | Native.WS_EX_LAYERED; + Native.SetWindowLong(Handle, Native.GWL_EXSTYLE, (IntPtr)ex); + + if (_magInitialized && !Blackout) + { + // Create the magnifier control as a child of this form + _magWindow = Native.CreateWindowEx( + 0, + "Magnifier", // WC_MAGNIFIER class + null, + Native.WS_CHILD | Native.WS_VISIBLE, + 0, + 0, + Width, + Height, + this.Handle, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + if (_magWindow != IntPtr.Zero) + { + RefreshMagnifierSource(); + var fx = Native.CreatePrivacyEffect(); + Native.MagSetColorEffect(_magWindow, ref fx); + + Native.SetWindowPos( + _magWindow, + IntPtr.Zero, + 0, + 0, + Width, + Height, + 0); + RefreshSource(); + } + } + } + + private void RefreshMagnifierSource() + { + if (_magWindow == IntPtr.Zero) return; + + var src = new Native.RECT + { + left = _screenBounds.Left, + top = _screenBounds.Top, + right = _screenBounds.Right, + bottom = _screenBounds.Bottom + }; + + Native.MagSetWindowSource(_magWindow, src); + } + + public void Toggle() + { + if (Visible) + { + Hide(); + } + else + { + if (!Blackout && _magWindow != IntPtr.Zero) + { + RefreshMagnifierSource(); + } + + Show(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + // Extra dark tint over whatever Magnifier is doing + if (Darken > 0) + { + int alpha = Math.Max(0, Math.Min(255, (int)(Darken * 2.55))); + using (SolidBrush brush = new SolidBrush(Color.FromArgb(alpha, 0, 0, 0))) + { + e.Graphics.FillRectangle(brush, ClientRectangle); + } + } + } + + protected override bool ShowWithoutActivation => true; + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + // We let the process exit clean up MagUninitialize; multiple overlays share the runtime. + } + } + } + +} diff --git a/PrivacyGlass/Native.cs b/PrivacyGlass/Native.cs new file mode 100644 index 0000000..5f8e537 --- /dev/null +++ b/PrivacyGlass/Native.cs @@ -0,0 +1,158 @@ +using System; +using System.Runtime.InteropServices; + +namespace PrivacyGlass +{ + internal static class Native + { + [Flags] + private enum DwmBlurBehindFlags : uint + { + DWM_BB_ENABLE = 0x00000001, + DWM_BB_BLURREGION = 0x00000002, + DWM_BB_TRANSITIONONMAXIMIZED = 0x00000004 + } + + [StructLayout(LayoutKind.Sequential)] + private struct DWM_BLURBEHIND + { + public DwmBlurBehindFlags dwFlags; + public bool fEnable; + public IntPtr hRgnBlur; + public bool fTransitionOnMaximized; + } + + [DllImport("dwmapi.dll")] + private static extern int DwmEnableBlurBehindWindow( + IntPtr hWnd, + ref DWM_BLURBEHIND pBlurBehind); + + + // Window styles + public const int WS_CHILD = 0x40000000; + public const int WS_VISIBLE = 0x10000000; + + // Extended styles + public const int WS_EX_TOPMOST = 0x00000008; + public const int WS_EX_LAYERED = 0x00080000; + public const int WS_EX_TRANSPARENT = 0x00000020; + + public const int GWL_EXSTYLE = -20; + + // ----- Magnification API ----- + + [DllImport("Magnification.dll", SetLastError = true)] + internal static extern bool MagInitialize(); + + [DllImport("Magnification.dll", SetLastError = true)] + internal static extern bool MagUninitialize(); + + [DllImport("Magnification.dll", SetLastError = true)] + internal static extern bool MagSetWindowSource(IntPtr hwnd, RECT rect); + + [DllImport("Magnification.dll", SetLastError = true)] + internal static extern bool MagSetColorEffect(IntPtr hwnd, ref MAGCOLOREFFECT effect); + + // ----- User32 stuff ----- + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern IntPtr CreateWindowEx( + int dwExStyle, + string lpClassName, + string lpWindowName, + int dwStyle, + int x, int y, + int nWidth, int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + + [DllImport("user32.dll")] + internal static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll")] + internal static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll")] + internal static extern bool SetWindowPos( + IntPtr hWnd, + IntPtr hWndInsertAfter, + int X, int Y, + int cx, int cy, + uint uFlags); + + // structures + + [StructLayout(LayoutKind.Sequential)] + internal struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct MAGCOLOREFFECT + { + // 5x5 color matrix (row-major) + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)] + public float[] transform; + } + + /// + /// Strong privacy colour effect: desaturated + darkened. + /// Not a geometric blur, but kills readability hard. + /// + internal static MAGCOLOREFFECT CreatePrivacyEffect() + { + var fx = new MAGCOLOREFFECT + { + transform = new float[25] + }; + + // We’ll map R,G,B all to the same gray value and darken it. + // Gray = (R+G+B)/3 * factor + float factor = 0.25f; // 0..1, lower = darker + float g = factor / 3f; + + // Row 0: R' + fx.transform[0] = g; // R + fx.transform[1] = g; // G + fx.transform[2] = g; // B + + // Row 1: G' + fx.transform[5] = g; + fx.transform[6] = g; + fx.transform[7] = g; + + // Row 2: B' + fx.transform[10] = g; + fx.transform[11] = g; + fx.transform[12] = g; + + // Row 3: A' = A + fx.transform[18] = 1.0f; + + // Row 4: w' (bias) – leave as identity + fx.transform[24] = 1.0f; + + return fx; + } + [DllImport("Magnification.dll", SetLastError = true)] + internal static extern bool MagSetWindowTransform( + IntPtr hwnd, + ref MAGTRANSFORM pTransform); + + [StructLayout(LayoutKind.Sequential)] + internal struct MAGTRANSFORM + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)] + public float[] v; // 3x3 matrix + } + + } + + +} diff --git a/PrivacyGlass/OverlayWindow.cs b/PrivacyGlass/OverlayWindow.cs new file mode 100644 index 0000000..25b72e8 --- /dev/null +++ b/PrivacyGlass/OverlayWindow.cs @@ -0,0 +1,149 @@ + +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Windows.Forms; + +namespace PrivacyGlass +{ + + public class OverlayWindow : Form + { + private Rectangle _screenBounds; + private Bitmap _blurred; + + // === Customizable settings === + public int Darken = 20; // 0 = no dark, 100 = full black + public bool BlurEnabled = true; // toggle blur (fast blackout vs frosted) + + public OverlayWindow(Rectangle bounds) + { + _screenBounds = bounds; + + FormBorderStyle = FormBorderStyle.None; + Bounds = bounds; + StartPosition = FormStartPosition.Manual; + TopMost = true; + ShowInTaskbar = false; + + DoubleBuffered = true; + BackgroundImageLayout = ImageLayout.Stretch; + BackColor = Color.Black; // fallback + Opacity = 1.0; + Visible = false; + } + + public void Toggle() + { + if (Visible) + { + Hide(); + } + else + { + RenderOverlay(); + Show(); + } + } + + private void RenderOverlay() + { + // === FULL BLACKOUT === + if (Darken >= 100) + { + // pure blackout, no screenshots + if (_blurred != null) { _blurred.Dispose(); _blurred = null; } + BackgroundImage = null; + return; + } + + // === otherwise capture the screen === + var bmp = new Bitmap(_screenBounds.Width, _screenBounds.Height, PixelFormat.Format32bppArgb); + using (var g = Graphics.FromImage(bmp)) + { + g.CopyFromScreen( + _screenBounds.X, + _screenBounds.Y, + 0, + 0, + _screenBounds.Size, + CopyPixelOperation.SourceCopy); + } + + // === BLUR if enabled === + if (BlurEnabled) + { + // dispose old + if (_blurred != null) { _blurred.Dispose(); _blurred = null; } + + // simple downscale blur + _blurred = DownscaleBlur(bmp, 8); + + bmp.Dispose(); + BackgroundImage = _blurred; + } + else + { + // NO BLUR — just display as-is + BackgroundImage = bmp; + } + } + + // Cheap fake blur (downscale → upscale) + private Bitmap DownscaleBlur(Bitmap source, int factor) + { + int w = Math.Max(1, source.Width / factor); + int h = Math.Max(1, source.Height / factor); + + var small = new Bitmap(w, h); + using (var g = Graphics.FromImage(small)) + { + g.InterpolationMode = InterpolationMode.HighQualityBilinear; + g.DrawImage(source, new Rectangle(0, 0, w, h)); + } + + var blurred = new Bitmap(source.Width, source.Height); + using (var g = Graphics.FromImage(blurred)) + { + g.InterpolationMode = InterpolationMode.HighQualityBilinear; + g.DrawImage(small, new Rectangle(0, 0, blurred.Width, blurred.Height)); + } + + small.Dispose(); + source.Dispose(); + return blurred; + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + // === DARKEN OVERLAY === + if (Darken > 0) + { + // convert 0-100 -> 0-255 alpha + int alpha = (int)(Darken * 2.55); + alpha = Math.Max(0, Math.Min(255, alpha)); + + using (var brush = new SolidBrush(Color.FromArgb(alpha, 0, 0, 0))) + { + e.Graphics.FillRectangle(brush, ClientRectangle); + } + } + } + + protected override bool ShowWithoutActivation => true; + + protected override void Dispose(bool disposing) + { + if (disposing && _blurred != null) + { + _blurred.Dispose(); + _blurred = null; + } + base.Dispose(disposing); + } + } + +} diff --git a/PrivacyGlass/PrivacyGlass.csproj b/PrivacyGlass/PrivacyGlass.csproj new file mode 100644 index 0000000..8a2ec5b --- /dev/null +++ b/PrivacyGlass/PrivacyGlass.csproj @@ -0,0 +1,96 @@ + + + + + Debug + AnyCPU + {71C41874-9B78-4484-82B9-9D5384C18E1D} + WinExe + PrivacyGlass + PrivacyGlass + v4.7.2 + 512 + true + true + + + x64 + true + full + true + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + Form + + + + Form + + + + + Form + + + + Form + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + + + + \ No newline at end of file diff --git a/PrivacyGlass/Program.cs b/PrivacyGlass/Program.cs new file mode 100644 index 0000000..7ee5426 --- /dev/null +++ b/PrivacyGlass/Program.cs @@ -0,0 +1,24 @@ + +using PrivacyGlass; +using System; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace PrivacyGlass +{ + internal static class Program + { + [DllImport("user32.dll")] + static extern bool SetProcessDpiAwarenessContext(int value); + + [STAThread] + static void Main() + { + SetProcessDpiAwarenessContext(-4); // PerMonitorV2 + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new TrayApp()); + } + } + +} diff --git a/PrivacyGlass/Properties/AssemblyInfo.cs b/PrivacyGlass/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..082c939 --- /dev/null +++ b/PrivacyGlass/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PrivacyGlass")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PrivacyGlass")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("71c41874-9b78-4484-82b9-9d5384c18e1d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/PrivacyGlass/Properties/Resources.Designer.cs b/PrivacyGlass/Properties/Resources.Designer.cs new file mode 100644 index 0000000..ab4625f --- /dev/null +++ b/PrivacyGlass/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PrivacyGlass.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PrivacyGlass.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/PrivacyGlass/Properties/Resources.resx b/PrivacyGlass/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/PrivacyGlass/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/PrivacyGlass/Properties/Settings.Designer.cs b/PrivacyGlass/Properties/Settings.Designer.cs new file mode 100644 index 0000000..44642f8 --- /dev/null +++ b/PrivacyGlass/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PrivacyGlass.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/PrivacyGlass/Properties/Settings.settings b/PrivacyGlass/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/PrivacyGlass/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/PrivacyGlass/Resources/privacyglass.ico b/PrivacyGlass/Resources/privacyglass.ico new file mode 100644 index 0000000000000000000000000000000000000000..a7e491a13184f896c4d397f3ed7ba581f2e05f00 GIT binary patch literal 193 zcmZQzU<5(|0R|vYU|0<##eldoz|WnRONtA~Iam+{XhOj@B@4Ktt0^v<-L)&mPxQ$xO`;mxM8BM)co&L&zYLn zl7gnPA&Z~Qdw=^cqxNR;p!7!^UF)`FIlB1Qn@ML}>@k;q^WX overlays = new List(); + //private const int HOTKEY_ID = 100; + + [DllImport("user32.dll")] + static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); + + [DllImport("user32.dll")] + static extern bool UnregisterHotKey(IntPtr hWnd, int id); + + private void RegisterGlobalHotKey() + { + // Ctrl + Alt + Space + RegisterHotKey( + IntPtr.Zero, + (int)HOTKEY_ID, + MOD_CONTROL | MOD_ALT, + (uint)Keys.Space + ); + } + + public TrayApp() + { + CreateTrayIcon(); + CreateOverlays(); + RegisterGlobalHotKey(); + Application.AddMessageFilter(new HotkeyFilter(Toggle)); + } + + + private void CreateTrayIcon() + { + var assembly = Assembly.GetExecutingAssembly(); + using (Stream s = assembly.GetManifestResourceStream("PrivacyGlass.Resources.privacyglass.ico")) + { + + Icon trayicon = new Icon(s); + + tray = new NotifyIcon() + { + Text = "PrivacyCurtain", + Icon = trayicon, + Visible = true, + ContextMenuStrip = BuildMenu() + }; + } + + } + + private ContextMenuStrip BuildMenu() + { + var menu = new ContextMenuStrip(); + + menu.Items.Add("Toggle Now", null, (_, __) => Toggle()); + menu.Items.Add("Settings...", null, ShowSettings); + menu.Items.Add("Exit", null, Exit); + + return menu; + } + + //private void CreateOverlays() + //{ + // foreach (var scr in Screen.AllScreens) + // overlays.Add(new MagnifyOverlay(scr.Bounds)); + //} + //private void CreateOverlays() + //{ + // foreach (var screen in Screen.AllScreens) + // { + // var ov = new MagnifyOverlay(screen.Bounds) + // { + // Blackout = false, // or true if you want pure black on this screen + // Darken = 50 // tweak tint strength + // }; + + // overlays.Add(ov); + // } + //} + private void CreateOverlays() + { + foreach (var screen in Screen.AllScreens) + { + BlurOverlay ov = new BlurOverlay(screen.Bounds); + ov.TintColor = Color.Black; + ov.TintOpacity = 12; // 0–255, higher = darker + + overlays.Add(ov); + } + } + + + private void Register() + { + // CTRL+ALT+B + RegisterHotKey(IntPtr.Zero, (int)HOTKEY_ID, 0x2 | 0x4, (uint)Keys.B); + Application.AddMessageFilter(new HotkeyFilter(Toggle)); + } + + private void Toggle() => + overlays.ForEach(o => o.Toggle()); + + private void Exit(object sender, EventArgs e) + { + UnregisterHotKey(IntPtr.Zero, (int)HOTKEY_ID); + tray.Visible = false; + Application.Exit(); + } + + private void ShowSettings(object sender, EventArgs e) + { + MessageBox.Show("Settings window goes here"); + } + } + +}