From c663731818d23c142320f41d98b1dc8bd1dbe691 Mon Sep 17 00:00:00 2001 From: Bailey Eaton Date: Mon, 15 May 2023 14:52:40 +1000 Subject: [PATCH] Release Candidate --- src/TED.sln | 28 ++++ src/TED/TED.DrawModes/DrawModeBase.cs | 33 +++++ src/TED/TED.DrawModes/OneTimeDrawMode.cs | 100 ++++++++++++++ src/TED/TED.Program/Options.cs | 91 ++++++++++++ src/TED/TED.Program/Program.cs | 159 +++++++++++++++++++++ src/TED/TED.Program/Tagger.cs | 40 ++++++ src/TED/TED.Utils/FileUtilities.cs | 77 +++++++++++ src/TED/TED.Utils/ImageUtilities.cs | 88 ++++++++++++ src/TED/TED.Utils/SystemUtilities.cs | 45 ++++++ src/TED/TED.Utils/Tokenizer.cs | 41 ++++++ src/TED/TED.Utils/Win32Native.cs | 169 +++++++++++++++++++++++ src/TED/TED.csproj | 40 ++++++ src/TED/app.manifest | 17 +++ 13 files changed, 928 insertions(+) create mode 100644 src/TED.sln create mode 100644 src/TED/TED.DrawModes/DrawModeBase.cs create mode 100644 src/TED/TED.DrawModes/OneTimeDrawMode.cs create mode 100644 src/TED/TED.Program/Options.cs create mode 100644 src/TED/TED.Program/Program.cs create mode 100644 src/TED/TED.Program/Tagger.cs create mode 100644 src/TED/TED.Utils/FileUtilities.cs create mode 100644 src/TED/TED.Utils/ImageUtilities.cs create mode 100644 src/TED/TED.Utils/SystemUtilities.cs create mode 100644 src/TED/TED.Utils/Tokenizer.cs create mode 100644 src/TED/TED.Utils/Win32Native.cs create mode 100644 src/TED/TED.csproj create mode 100644 src/TED/app.manifest diff --git a/src/TED.sln b/src/TED.sln new file mode 100644 index 0000000..10d06ef --- /dev/null +++ b/src/TED.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32630.192 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TED", "TED\TED.csproj", "{BD69CE02-1B63-4949-B34B-AA6E875018DA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Open|Any CPU = Open|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BD69CE02-1B63-4949-B34B-AA6E875018DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD69CE02-1B63-4949-B34B-AA6E875018DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD69CE02-1B63-4949-B34B-AA6E875018DA}.Open|Any CPU.ActiveCfg = Open|Any CPU + {BD69CE02-1B63-4949-B34B-AA6E875018DA}.Open|Any CPU.Build.0 = Open|Any CPU + {BD69CE02-1B63-4949-B34B-AA6E875018DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD69CE02-1B63-4949-B34B-AA6E875018DA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CEDB2C21-E30F-4966-9C5F-7A936F90E393} + EndGlobalSection +EndGlobal diff --git a/src/TED/TED.DrawModes/DrawModeBase.cs b/src/TED/TED.DrawModes/DrawModeBase.cs new file mode 100644 index 0000000..5748717 --- /dev/null +++ b/src/TED/TED.DrawModes/DrawModeBase.cs @@ -0,0 +1,33 @@ +using System; +using TED.Program; + +namespace TED.DrawModes +{ + /// + /// Serves as the base class for different drawing modes. + /// + internal abstract class DrawModeBase + { + // The device context to be drawn on + protected IntPtr DeviceContext; + + // The options to be used when drawing + protected readonly Options Options; + + /// + /// Initializes a new instance of the DrawModeBase class with the specified device context and options. + /// + /// The device context to be drawn on. + /// The options to be used when drawing. + internal DrawModeBase(IntPtr deviceContext, Options options) + { + DeviceContext = deviceContext; + Options = options; + } + + /// + /// When overridden in a derived class, performs the drawing action. + /// + public abstract void Draw(); + } +} diff --git a/src/TED/TED.DrawModes/OneTimeDrawMode.cs b/src/TED/TED.DrawModes/OneTimeDrawMode.cs new file mode 100644 index 0000000..fff28bd --- /dev/null +++ b/src/TED/TED.DrawModes/OneTimeDrawMode.cs @@ -0,0 +1,100 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using TED.Program; +using TED.Utils; + +namespace TED.DrawModes +{ + /// + /// Represents a drawing mode that operates once and does not update. + /// + internal class OneTimeDrawMode : DrawModeBase + { + /// + /// Initializes a new instance of the OneTimeDrawMode class with the specified device context and options. + /// + /// The device context to be drawn on. + /// The options to be used when drawing. + public OneTimeDrawMode(IntPtr deviceContext, Options options) : base(deviceContext, options) + { + } + + /// + /// Draws the image and text on the given device context based on the options provided. + /// + public override void Draw() + { + // Calculate the luminance of the wallpaper + var wallpaperLuminance = ImageUtilities.CalculateWallpaperLuminance(); + var primaryAreaRect = SystemUtilities.GetPrimaryScreenRect(); + + if (DeviceContext != IntPtr.Zero) + { + using (var graphics = Graphics.FromHdc(DeviceContext)) + { + // Get the path of the image based on the luminance of the wallpaper + var imagePath = Options.GetImagePath(wallpaperLuminance); + + // Set the text color based on the luminance of the wallpaper + var textColor = wallpaperLuminance > 0.5 ? Color.Black : Color.White; + + using (var font = new Font(Options.FontName, Options.FontSize, FontStyle.Bold)) + { + // Calculate the scaling factors + var scaleX = graphics.DpiX / 96.0f; + var scaleY = graphics.DpiY / 96.0f; + + // Calculate the working area dimensions + var scaledWorkingAreaWidth = primaryAreaRect.X / scaleX; + var scaledWorkingAreaHeight = primaryAreaRect.Y / scaleY; + + // Calculate the maximum width of all lines + var maxWidth = graphics.MeasureString(Options.Lines.Max(l => l), font).Width; + + // Calculate the positions of the text and the image + var textX = scaledWorkingAreaWidth + Screen.PrimaryScreen.WorkingArea.Width - maxWidth - Options.PaddingHorizontal; + var textY = scaledWorkingAreaHeight + Screen.PrimaryScreen.WorkingArea.Height - Options.Lines.Sum(line => graphics.MeasureString(line, font).Height) - (Options.LineSpacing * (Options.Lines.Count - 1)) - Options.PaddingVertical; + + if (!string.IsNullOrEmpty(imagePath)) + { + using (var overlayImage = Image.FromFile(imagePath)) + { + // Scale the image while maintaining its aspect ratio + ImageUtilities.ScaleImageAndMaintainAspectRatio(overlayImage.Width, overlayImage.Height, maxWidth, int.MaxValue, out int newWidth, out int newHeight); + + var imageX = scaledWorkingAreaWidth + Screen.PrimaryScreen.WorkingArea.Width - newWidth - Options.PaddingHorizontal; + var imageY = scaledWorkingAreaHeight + Screen.PrimaryScreen.WorkingArea.Height - newHeight - Options.Lines.Sum(line => graphics.MeasureString(line, font).Height) - (Options.LineSpacing * (Options.Lines.Count)) - Options.PaddingVertical; + + textX = imageX; + textY = imageY + newHeight + Options.LineSpacing; + + // Draw the image + graphics.DrawImage(overlayImage, new RectangleF(imageX, imageY, newWidth, newHeight)); + } + } + + // Draw each line of text + for (var i = 0; i < Options.Lines.Count; i++) + { + var line = Options.Lines[i]; + + // Draw the line + graphics.DrawString(line, font, new SolidBrush(textColor), new PointF(textX, textY)); + + // Move the text cursor down to the next line + textY += graphics.MeasureString(line, font).Height; + + // If there are more lines, add additional spacing + if (i < Options.Lines.Count) + { + textY += Options.LineSpacing; + } + } + } + } + } + } + } +} diff --git a/src/TED/TED.Program/Options.cs b/src/TED/TED.Program/Options.cs new file mode 100644 index 0000000..38ff6ca --- /dev/null +++ b/src/TED/TED.Program/Options.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using TED.Utils; + +namespace TED.Program +{ + /// + /// Container for the various options utilized within TED. + /// + internal class Options + { + internal readonly int PaddingHorizontal; + internal readonly int PaddingVertical; + internal readonly int LineSpacing; + internal readonly int FontSize; + internal readonly string FontName; + internal readonly string ImagePath; + internal readonly string LightImagePath; + internal readonly string DarkImagePath; + internal readonly List Lines; + internal readonly bool Debug; + internal readonly bool AdaptiveImageMode; + + /// + /// Gets default options + /// + private static Options? _default; + internal static Options Default + { + get + { + _default ??= new Options( + 10, 10, 8, 8, "Arial", + "", "", "", + new List() + { + Tokenizer.ReplaceTokens("USERNAME: @userName"), + Tokenizer.ReplaceTokens("DEVICE NAME: @machineName"), + Tokenizer.ReplaceTokens("OS: @os"), + }, + false); + + return _default; + } + } + + /// + /// Initializes a new instance of the Options class with the specified settings. + /// + internal Options(int paddingHorizontal, int paddingVertical, + int lineSpacing, int fontSize, string fontName, + string imagePath, string lightImagePath, + string darkImagePath, List lines, + bool debug) + { + PaddingHorizontal = paddingHorizontal; + PaddingVertical = paddingVertical; + LineSpacing = lineSpacing; + FontSize = fontSize; + FontName = fontName; + ImagePath = imagePath; + LightImagePath = lightImagePath; + DarkImagePath = darkImagePath; + Lines = lines; + Debug = debug; + AdaptiveImageMode = !string.IsNullOrEmpty(LightImagePath) && !string.IsNullOrEmpty(DarkImagePath); + } + + /// + /// Determines the image path based on the wallpaper's luminance. + /// When in AdaptiveImageMode, chooses the light or dark image path based on the provided luminance. + /// + /// Note: AdaptiveImageMode requires both a light image and dark image to be set. If only one is provided, the regular image + /// path will be used as a fallback. + /// + /// The luminance of the wallpaper. + /// + /// The appropriate image path considering the luminance and the AdaptiveImageMode. + /// + internal string GetImagePath(double wallpaperLuminance) + { + if (AdaptiveImageMode && !string.IsNullOrEmpty(LightImagePath) && !string.IsNullOrEmpty(DarkImagePath)) + { + return wallpaperLuminance > 0.5 ? DarkImagePath : LightImagePath; + } + else + { + return ImagePath; + } + } + } +} diff --git a/src/TED/TED.Program/Program.cs b/src/TED/TED.Program/Program.cs new file mode 100644 index 0000000..7d71d09 --- /dev/null +++ b/src/TED/TED.Program/Program.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using TED.Utils; + +namespace TED.Program +{ + /// + /// The main class for the program. + /// + public class Program + { + + private static Tagger? tagger; + + /// + /// Application entry point + /// + /// An array of command-line arguments. + public static void Main(string[] args) + { + tagger = new Tagger(ParseArgsIntoOptions(args)); + tagger.Tag(); + } + + /// + /// Parses the command-line arguments into an Options object. + /// + /// An array of command-line arguments. + /// An Options object that encapsulates application settings. + private static Options ParseArgsIntoOptions(string[] args) + { + var fontName = GetArgument(args, new string[] { "-font", "-f" }, Options.Default.FontName); + var imagePath = GetArgument(args, new string[] { "-image", "-i" }, Options.Default.ImagePath); + var darkImagePath = GetArgument(args, new string[] { "-darkimage", "-di" }, Options.Default.DarkImagePath); + var lightImagePath = GetArgument(args, new string[] { "-lightimage", "-li" }, Options.Default.LightImagePath); + var lines = Options.Default.Lines; + + if (!bool.TryParse(GetArgument(args, new string[] { "-debug", "-d" }, Options.Default.Debug.ToString()), out bool debug)) + { + debug = Options.Default.Debug; + } + + if (!int.TryParse(GetArgument(args, new string[] { "-fontsize", "-fs" }, Options.Default.FontSize.ToString()), out int fontSize)) + { + fontSize = Options.Default.FontSize; + } + + if (!int.TryParse(GetArgument(args, new string[] { "-linespacing", "-ls" }, Options.Default.LineSpacing.ToString()), out int margin)) + { + margin = Options.Default.LineSpacing; + } + + if (!int.TryParse(GetArgument(args, new string[] { "-hpad", "-hp" }, Options.Default.PaddingHorizontal.ToString()), out int paddingHorizontal)) + { + paddingHorizontal = Options.Default.PaddingHorizontal; + } + + if (!int.TryParse(GetArgument(args, new string[] { "-vpad", "-vp" }, Options.Default.PaddingHorizontal.ToString()), out int paddingVertical)) + { + paddingVertical = Options.Default.PaddingVertical; + } + + if (args.Any(arg => arg.Contains("-line"))) + { + lines.Clear(); + } + + for (int i = 0; i < args.Length; i++) + { + if (args[i] == "-line") + { + + for (int j = i + 1; j < args.Length; j++) + { + if (args[j].StartsWith("-")) + { + break; + } + + lines.Add(Tokenizer.ReplaceTokens(args[j])); + } + } + } + + imagePath = string.IsNullOrEmpty(imagePath) ? imagePath : EnsureImageExists(imagePath); + lightImagePath = string.IsNullOrEmpty(lightImagePath) ? lightImagePath : EnsureImageExists(lightImagePath); + darkImagePath = string.IsNullOrEmpty(darkImagePath) ? darkImagePath : EnsureImageExists(darkImagePath); + + return new Options( + paddingHorizontal, + paddingVertical, + margin, + fontSize, + fontName, + imagePath, + lightImagePath, + darkImagePath, + lines, + debug + ); + } + + /// + /// Validates the existence of the image file at the specified path. + /// + /// Path of the image file. + /// + /// The path of the image. If the image path points to a URL, the method attempts to download + /// the image, cache it, and then return the local path of the downloaded image. + /// + /// + /// Thrown if the image path points to a URL and the image fails to download. + /// + /// + /// Thrown if the image path does not point to a valid local file or URL. + /// + private static string EnsureImageExists(string imagePath) + { + if (!FileUtilities.PathIsLocalFile(imagePath)) + { + if (FileUtilities.PathIsUrl(imagePath)) + { + var path = string.Empty; + + try + { + path = FileUtilities.DownloadAndCacheFileAsync(imagePath).GetAwaiter().GetResult(); + } + catch + { + throw new HttpRequestException("ERROR: Failed to acquire image from provided URL"); + } + + return path; + } + else + { + throw new FileNotFoundException("ERROR: Image filepath or URL is invalid."); + } + } + + return imagePath; + } + + /// + /// Retrieves the value of a specific command-line argument. + /// + /// A collection of command-line arguments. + /// An array of options to match against the command-line arguments. + /// The default value to return if the option is not found. + /// The value of the specified command-line argument, or the default value if the option is not found. + private static string GetArgument(IEnumerable args, string[] options, string defaultValue) => + args.SkipWhile(i => !options.Contains(i)).Skip(1).Take(1).DefaultIfEmpty(defaultValue).First(); + } +} + diff --git a/src/TED/TED.Program/Tagger.cs b/src/TED/TED.Program/Tagger.cs new file mode 100644 index 0000000..a53bb73 --- /dev/null +++ b/src/TED/TED.Program/Tagger.cs @@ -0,0 +1,40 @@ +using TED.DrawModes; +using TED.Utils; + +namespace TED.Program +{ + /// + /// Provides functionality for tagging windows in the system. + /// + internal class Tagger + { + /// + /// Provides functionality for tagging windows in the system. + /// + internal Options Options; + + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The options to be used while tagging. + public Tagger(Options options) + { + Options = options; + + } + + /// + /// Tags the window based on the given . + /// + public void Tag() + { + var progman = Win32Native.GetProgmanWindow(); + Win32Native.ClearWindow(progman); + var workerWindow = Win32Native.CreateWorkerW(progman); + var deviceContext = Win32Native.GetWorkerWindowDeviceContext(workerWindow); + DrawModeBase drawer = new OneTimeDrawMode(deviceContext, Options); + drawer.Draw(); + Win32Native.ReleaseWorkerWindowDeviceContext(workerWindow, deviceContext); + } + } +} diff --git a/src/TED/TED.Utils/FileUtilities.cs b/src/TED/TED.Utils/FileUtilities.cs new file mode 100644 index 0000000..d9acb99 --- /dev/null +++ b/src/TED/TED.Utils/FileUtilities.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace TED.Utils +{ + /// + /// Provides utility methods for working with files + /// + internal class FileUtilities + { + /// + /// Downloads a file from a specified URL and saves it to a cache. + /// If the file already exists in the cache, the function returns the path to the cached file. + /// If the file does not exist in the cache, the function downloads the file, saves it to the cache, and then returns the path to the cached file. + /// + /// The URL of the file to download. + /// A task that represents the asynchronous operation. The task result is the path to the downloaded (or cached) file. + public static async Task DownloadAndCacheFileAsync(string url) + { + using (var client = new HttpClient() + { + Timeout = TimeSpan.FromSeconds(10) + }) + { + var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var etag = response.Headers.ETag?.Tag.Replace("\"", string.Empty) ?? "untagged"; + if (!Directory.Exists(Path.Combine(Path.GetTempPath(), "TED"))) + { + Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "TED")); + } + var path = Path.Combine(Path.GetTempPath(), "TED", $"{etag}.png"); + + if (File.Exists(path)) + { + return path; + } + + var filesToDelete = Directory.GetFiles(Path.Combine(Path.GetTempPath(), "TED"), "*.png") + .Where(filePath => Path.GetFileNameWithoutExtension(filePath) != etag); + foreach (var fileToDelete in filesToDelete) + { + File.Delete(fileToDelete); + } + + using (var stream = await response.Content.ReadAsStreamAsync()) + { + using (var fileStream = new FileStream(path, FileMode.CreateNew)) + { + await stream.CopyToAsync(fileStream); + return path; + } + } + } + } + + /// + /// Downloads a file from a specified URL and saves it to a cache. + /// If the file already exists in the cache, the function returns the path to the cached file. + /// If the file does not exist in the cache, the function downloads the file, saves it to the cache, and then returns the path to the cached file. + /// + /// The URL of the file to download. + /// A task that represents the asynchronous operation. The task result is the path to the downloaded (or cached) file. + public static bool PathIsLocalFile(string path) => Path.IsPathFullyQualified(path) && File.Exists(path); + + /// + /// Determines whether a specified path represents a URL. + /// + /// The path to check. + /// True if the path represents a URL; otherwise, false. + public static bool PathIsUrl(string path) => !Path.IsPathFullyQualified(path) && Uri.TryCreate(path, UriKind.Absolute, out _); + } +} diff --git a/src/TED/TED.Utils/ImageUtilities.cs b/src/TED/TED.Utils/ImageUtilities.cs new file mode 100644 index 0000000..610fe4c --- /dev/null +++ b/src/TED/TED.Utils/ImageUtilities.cs @@ -0,0 +1,88 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; + +namespace TED.Utils +{ + /// + /// Provides utility methods for working with images + /// + public class ImageUtilities + { + /// + /// Scales the image dimensions to fit within the specified maximum width and height, maintaining the original aspect ratio. + /// + /// The original image width. + /// The original image height. + /// The maximum allowable width for the scaled image. + /// The maximum allowable height for the scaled image. + /// The calculated width for the scaled image. + /// The calculated height for the scaled image. + public static void ScaleImageAndMaintainAspectRatio(int srcWidth, int srcHeight, float maxWidth, float maxHeight, out int destWidth, out int destHeight) + { + var ratioX = (double)maxWidth / srcWidth; + var ratioY = (double)maxHeight / srcHeight; + var ratio = Math.Min(ratioX, ratioY); + + destWidth = (int)(srcWidth * ratio); + destHeight = (int)(srcHeight * ratio); + } + + /// + /// Calculates the perceived luminance of the current desktop wallpaper. + /// + /// A value representing the calculated perceived luminance of the wallpaper. Returns 0.0 if the wallpaper path is not found. + public static double CalculateWallpaperLuminance() + { + // Get the wallpaper path from the registry + string wallpaperPath = SystemUtilities.GetWallpaperPathFromRegistry(); + double luminance = 0.0; + + if (!string.IsNullOrEmpty(wallpaperPath)) + { + using (var bmp = new Bitmap(wallpaperPath)) + { + luminance = CalculateImageLuminance01(bmp); + } + } + + return luminance; + } + + /// + /// Calculates the perceived luminance of a Bitmap image. + /// + /// The Bitmap image to calculate the luminance for. + /// A value representing the calculated luminance of the image. + public static double CalculateImageLuminance01(Bitmap bm) + { + var lum = 0.0; + var width = bm.Width; + var height = bm.Height; + var bytesPerPixel = Image.GetPixelFormatSize(bm.PixelFormat) / 8; + + var srcData = bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.ReadOnly, bm.PixelFormat); + var stride = srcData.Stride; + IntPtr scan0 = srcData.Scan0; + + // Luminance (standard, objective): (0.2126*R) + (0.7152*G) + (0.0722*B) + // Luminance (perceived option 1): (0.299*R + 0.587*G + 0.114*B) + // Luminance (perceived option 2, slower to calculate): sqrt( 0.299*R^2 + 0.587*G^2 + 0.114*B^2 ) + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var idx = y * stride + x * bytesPerPixel; + lum += (0.299 * Marshal.ReadByte(scan0, idx + 2) + 0.587 * Marshal.ReadByte(scan0, idx + 1) + 0.114 * Marshal.ReadByte(scan0, idx)) / 255.0; + } + } + + bm.UnlockBits(srcData); + var normalized = lum / (width * height); + return normalized; + } + } + +} \ No newline at end of file diff --git a/src/TED/TED.Utils/SystemUtilities.cs b/src/TED/TED.Utils/SystemUtilities.cs new file mode 100644 index 0000000..5e043df --- /dev/null +++ b/src/TED/TED.Utils/SystemUtilities.cs @@ -0,0 +1,45 @@ +using Microsoft.Win32; +using System.Drawing; +using System.Windows.Forms; + +namespace TED.Utils +{ + /// + /// Provides utility functions related to system configurations. + /// + public class SystemUtilities + { + /// + /// Get's the Rectangle of the Primary Screen in Virtual Screen space. + /// + /// The Rectangle of the Primary Screen in Virtual Screen space. + public static Rectangle GetPrimaryScreenRect() + { + return new Rectangle( + Screen.PrimaryScreen.Bounds.X - SystemInformation.VirtualScreen.Left, + Screen.PrimaryScreen.Bounds.Y - SystemInformation.VirtualScreen.Top, + Screen.PrimaryScreen.WorkingArea.Width, + Screen.PrimaryScreen.WorkingArea.Height + ); + } + + /// + /// Fetches the current wallpaper path from the system registry. + /// + /// The full path of the current desktop wallpaper. + public static string GetWallpaperPathFromRegistry() + { + string result = string.Empty; + using (var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Desktop")) + { + if (key != null) + { + result = key.GetValue("Wallpaper").ToString(); + key.Close(); + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/TED/TED.Utils/Tokenizer.cs b/src/TED/TED.Utils/Tokenizer.cs new file mode 100644 index 0000000..5a2af27 --- /dev/null +++ b/src/TED/TED.Utils/Tokenizer.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Security.Principal; + +namespace TED.Utils +{ + /// + /// Provides functionality for replacing tokens in a string. + /// + internal class Tokenizer + { + /// + /// A dictionary that maps token names to their corresponding values. + /// + static Dictionary> TokenLookup = new Dictionary>() + { + { "@userName", () => WindowsIdentity.GetCurrent().Name }, + { "@machineName", () => Environment.MachineName }, + { "@os", () => Environment.OSVersion.ToString() }, + }; + + /// + /// Replaces tokens in the input string with their corresponding values. + /// + /// The string that may contain tokens to be replaced. + /// The input string with all tokens replaced by their corresponding values. + public static string ReplaceTokens(string input) + { + foreach (var kvp in TokenLookup) + { + var token = kvp.Key; + var value = kvp.Value(); + + input = input.Replace(token, value); + } + + return input; + } + } + +} \ No newline at end of file diff --git a/src/TED/TED.Utils/Win32Native.cs b/src/TED/TED.Utils/Win32Native.cs new file mode 100644 index 0000000..5af941b --- /dev/null +++ b/src/TED/TED.Utils/Win32Native.cs @@ -0,0 +1,169 @@ +using System; +using System.Runtime.InteropServices; + +namespace TED.Utils +{ + /// + /// + /// The information and methods stored within this class are largely attributed to + /// It has been a fantastic resource for tinkering with and learning how native methods work. + /// + /// + + public class Win32Native + { + public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); + + [Flags()] + public enum DeviceContextValues : uint + { + /// DCX_WINDOW: Returns a DC that corresponds to the window rectangle rather + /// than the client rectangle. + Window = 0x00000001, + + /// DCX_CACHE: Returns a DC from the cache, rather than the OWNDC or CLASSDC + /// window. Essentially overrides CS_OWNDC and CS_CLASSDC. + Cache = 0x00000002, + + /// DCX_NORESETATTRS: Does not reset the attributes of this DC to the + /// default attributes when this DC is released. + NoResetAttrs = 0x00000004, + + /// DCX_CLIPCHILDREN: Excludes the visible regions of all child windows + /// below the window identified by hWnd. + ClipChildren = 0x00000008, + + /// DCX_CLIPSIBLINGS: Excludes the visible regions of all sibling windows + /// above the window identified by hWnd. + ClipSiblings = 0x00000010, + + /// DCX_PARENTCLIP: Uses the visible region of the parent window. The + /// parent's WS_CLIPCHILDREN and CS_PARENTDC style bits are ignored. The origin is + /// set to the upper-left corner of the window identified by hWnd. + ParentClip = 0x00000020, + + /// DCX_EXCLUDERGN: The clipping region identified by hrgnClip is excluded + /// from the visible region of the returned DC. + ExcludeRgn = 0x00000040, + + /// DCX_INTERSECTRGN: The clipping region identified by hrgnClip is + /// intersected with the visible region of the returned DC. + IntersectRgn = 0x00000080, + + /// DCX_EXCLUDEUPDATE: Unknown...Undocumented + ExcludeUpdate = 0x00000100, + + /// DCX_INTERSECTUPDATE: Unknown...Undocumented + IntersectUpdate = 0x00000200, + + /// DCX_LOCKWINDOWUPDATE: Allows drawing even if there is a LockWindowUpdate + /// call in effect that would otherwise exclude this window. Used for drawing during + /// tracking. + LockWindowUpdate = 0x00000400, + + /// DCX_USESTYLE: Undocumented, something related to WM_NCPAINT message. + UseStyle = 0x00010000, + + /// DCX_VALIDATE When specified with DCX_INTERSECTUPDATE, causes the DC to + /// be completely validated. Using this function with both DCX_INTERSECTUPDATE and + /// DCX_VALIDATE is identical to using the BeginPaint function. + Validate = 0x00200000, + } + + [Flags] + public enum SendMessageTimeoutFlags : uint + { + SMTO_NORMAL = 0x0, + SMTO_BLOCK = 0x1, + SMTO_ABORTIFHUNG = 0x2, + SMTO_NOTIMEOUTIFNOTHUNG = 0x8, + SMTO_ERRORONEXIT = 0x20 + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + //Keep + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, IntPtr windowTitle); + + [DllImport("user32.dll")] + public static extern IntPtr GetDCEx(IntPtr hWnd, IntPtr hrgnClip, DeviceContextValues flags); + + [DllImport("user32.dll", EntryPoint = "ReleaseDC")] + public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDc); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, int wParam, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageTimeout(IntPtr windowHandle, uint Msg, IntPtr wParam, IntPtr lParam, SendMessageTimeoutFlags flags, uint timeout, out IntPtr result); + + public static IntPtr GetProgmanWindow() + { + // We're going to render the watermark over the wallpaper, but behind desktop icons. + // We can do so with some trickery using the Progman window + // We're going to spawn a WorkerW window behind the desktop icons, and then render our watermark to that + return FindWindow("Progman", null); + } + + public static void ClearWindow(IntPtr windowHandle) + { + // The below command clears any existing watermark we've drawn + // The Thread.Sleep() is necessary because the command has a minor delay to it + // Without it, the clear will end up running after we've generated a watermark, + // removing our newly generated watermark + SendMessage(windowHandle, 0x0034, 4, IntPtr.Zero); + //Thread.Sleep(2000); + } + + public static IntPtr CreateWorkerW(IntPtr progman) + { + IntPtr result = IntPtr.Zero; + + // Send 0x052C to Progman. This message directs Progman to spawn a + // WorkerW behind the desktop icons. If it is already there, nothing + // happens. + SendMessageTimeout(progman, + 0x052C, + new IntPtr(0), + IntPtr.Zero, + SendMessageTimeoutFlags.SMTO_NORMAL, + 1000, + out result); + + IntPtr workerw = IntPtr.Zero; + + // We enumerate all Windows, until we find one, that has the SHELLDLL_DefView + // as a child. These should be our desktop icons. + // If we found that window, we take its next sibling and assign it to workerw. + EnumWindows(new EnumWindowsProc((tophandle, topparamhandle) => + { + IntPtr p = FindWindowEx(tophandle, + IntPtr.Zero, + "SHELLDLL_DefView", + IntPtr.Zero); + + if (p != IntPtr.Zero) + { + // Gets the WorkerW Window after the current one. + workerw = FindWindowEx(IntPtr.Zero, + tophandle, + "WorkerW", + IntPtr.Zero); + } + + return true; + }), IntPtr.Zero); + + return workerw; + } + + public static IntPtr GetWorkerWindowDeviceContext(IntPtr workerw) => GetDCEx(workerw, IntPtr.Zero, (DeviceContextValues)0x403); + public static void ReleaseWorkerWindowDeviceContext(IntPtr workerw, IntPtr deviceContext) => ReleaseDC(workerw, deviceContext); + } +} \ No newline at end of file diff --git a/src/TED/TED.csproj b/src/TED/TED.csproj new file mode 100644 index 0000000..ff02c98 --- /dev/null +++ b/src/TED/TED.csproj @@ -0,0 +1,40 @@ + + + true + WinExe + net7.0-windows10.0.22621.0 + true + true + win-x64 + true + true + Link + TED + enable + Debug;Release;Open + 1.0.4.1-RC1 + Prerelease + true + True + app.manifest + true + Health IT + TED - Tag Every Desktop + Health IT + + + True + + + False + + + + + + + + + + + diff --git a/src/TED/app.manifest b/src/TED/app.manifest new file mode 100644 index 0000000..1aa0b81 --- /dev/null +++ b/src/TED/app.manifest @@ -0,0 +1,17 @@ + + + + + + + + + + + + + true + true + + +