Release Candidate
This commit is contained in:
parent
9c048ec7fe
commit
c663731818
28
src/TED.sln
Normal file
28
src/TED.sln
Normal file
@ -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
|
||||
33
src/TED/TED.DrawModes/DrawModeBase.cs
Normal file
33
src/TED/TED.DrawModes/DrawModeBase.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using TED.Program;
|
||||
|
||||
namespace TED.DrawModes
|
||||
{
|
||||
/// <summary>
|
||||
/// Serves as the base class for different drawing modes.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the DrawModeBase class with the specified device context and options.
|
||||
/// </summary>
|
||||
/// <param name="deviceContext">The device context to be drawn on.</param>
|
||||
/// <param name="options">The options to be used when drawing.</param>
|
||||
internal DrawModeBase(IntPtr deviceContext, Options options)
|
||||
{
|
||||
DeviceContext = deviceContext;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When overridden in a derived class, performs the drawing action.
|
||||
/// </summary>
|
||||
public abstract void Draw();
|
||||
}
|
||||
}
|
||||
100
src/TED/TED.DrawModes/OneTimeDrawMode.cs
Normal file
100
src/TED/TED.DrawModes/OneTimeDrawMode.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a drawing mode that operates once and does not update.
|
||||
/// </summary>
|
||||
internal class OneTimeDrawMode : DrawModeBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the OneTimeDrawMode class with the specified device context and options.
|
||||
/// </summary>
|
||||
/// <param name="deviceContext">The device context to be drawn on.</param>
|
||||
/// <param name="options">The options to be used when drawing.</param>
|
||||
public OneTimeDrawMode(IntPtr deviceContext, Options options) : base(deviceContext, options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the image and text on the given device context based on the options provided.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/TED/TED.Program/Options.cs
Normal file
91
src/TED/TED.Program/Options.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
using TED.Utils;
|
||||
|
||||
namespace TED.Program
|
||||
{
|
||||
/// <summary>
|
||||
/// Container for the various options utilized within TED.
|
||||
/// </summary>
|
||||
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<string> Lines;
|
||||
internal readonly bool Debug;
|
||||
internal readonly bool AdaptiveImageMode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets default options
|
||||
/// </summary>
|
||||
private static Options? _default;
|
||||
internal static Options Default
|
||||
{
|
||||
get
|
||||
{
|
||||
_default ??= new Options(
|
||||
10, 10, 8, 8, "Arial",
|
||||
"", "", "",
|
||||
new List<string>()
|
||||
{
|
||||
Tokenizer.ReplaceTokens("USERNAME: @userName"),
|
||||
Tokenizer.ReplaceTokens("DEVICE NAME: @machineName"),
|
||||
Tokenizer.ReplaceTokens("OS: @os"),
|
||||
},
|
||||
false);
|
||||
|
||||
return _default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Options class with the specified settings.
|
||||
/// </summary>
|
||||
internal Options(int paddingHorizontal, int paddingVertical,
|
||||
int lineSpacing, int fontSize, string fontName,
|
||||
string imagePath, string lightImagePath,
|
||||
string darkImagePath, List<string> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="wallpaperLuminance">The luminance of the wallpaper.</param>
|
||||
/// <returns>
|
||||
/// The appropriate image path considering the luminance and the AdaptiveImageMode.
|
||||
/// </returns>
|
||||
internal string GetImagePath(double wallpaperLuminance)
|
||||
{
|
||||
if (AdaptiveImageMode && !string.IsNullOrEmpty(LightImagePath) && !string.IsNullOrEmpty(DarkImagePath))
|
||||
{
|
||||
return wallpaperLuminance > 0.5 ? DarkImagePath : LightImagePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ImagePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/TED/TED.Program/Program.cs
Normal file
159
src/TED/TED.Program/Program.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The main class for the program.
|
||||
/// </summary>
|
||||
public class Program
|
||||
{
|
||||
|
||||
private static Tagger? tagger;
|
||||
|
||||
/// <summary>
|
||||
/// Application entry point
|
||||
/// </summary>
|
||||
/// <param name="args">An array of command-line arguments.</param>
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
tagger = new Tagger(ParseArgsIntoOptions(args));
|
||||
tagger.Tag();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the command-line arguments into an Options object.
|
||||
/// </summary>
|
||||
/// <param name="args">An array of command-line arguments.</param>
|
||||
/// <returns>An Options object that encapsulates application settings.</returns>
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the existence of the image file at the specified path.
|
||||
/// </summary>
|
||||
/// <param name="imagePath">Path of the image file.</param>
|
||||
/// <returns>
|
||||
/// 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.
|
||||
/// </returns>
|
||||
/// <exception cref="HttpRequestException">
|
||||
/// Thrown if the image path points to a URL and the image fails to download.
|
||||
/// </exception>
|
||||
/// <exception cref="FileNotFoundException">
|
||||
/// Thrown if the image path does not point to a valid local file or URL.
|
||||
/// </exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the value of a specific command-line argument.
|
||||
/// </summary>
|
||||
/// <param name="args">A collection of command-line arguments.</param>
|
||||
/// <param name="options">An array of options to match against the command-line arguments.</param>
|
||||
/// <param name="defaultValue">The default value to return if the option is not found.</param>
|
||||
/// <returns>The value of the specified command-line argument, or the default value if the option is not found.</returns>
|
||||
private static string GetArgument(IEnumerable<string> args, string[] options, string defaultValue) =>
|
||||
args.SkipWhile(i => !options.Contains(i)).Skip(1).Take(1).DefaultIfEmpty(defaultValue).First();
|
||||
}
|
||||
}
|
||||
|
||||
40
src/TED/TED.Program/Tagger.cs
Normal file
40
src/TED/TED.Program/Tagger.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using TED.DrawModes;
|
||||
using TED.Utils;
|
||||
|
||||
namespace TED.Program
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides functionality for tagging windows in the system.
|
||||
/// </summary>
|
||||
internal class Tagger
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides functionality for tagging windows in the system.
|
||||
/// </summary>
|
||||
internal Options Options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Tagger"/> class with the specified options.
|
||||
/// </summary>
|
||||
/// <param name="options">The options to be used while tagging.</param>
|
||||
public Tagger(Options options)
|
||||
{
|
||||
Options = options;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tags the window based on the given <see cref="Options"/>.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/TED/TED.Utils/FileUtilities.cs
Normal file
77
src/TED/TED.Utils/FileUtilities.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TED.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides utility methods for working with files
|
||||
/// </summary>
|
||||
internal class FileUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the file to download.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result is the path to the downloaded (or cached) file.</returns>
|
||||
public static async Task<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the file to download.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result is the path to the downloaded (or cached) file.</returns>
|
||||
public static bool PathIsLocalFile(string path) => Path.IsPathFullyQualified(path) && File.Exists(path);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a specified path represents a URL.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
/// <returns>True if the path represents a URL; otherwise, false.</returns>
|
||||
public static bool PathIsUrl(string path) => !Path.IsPathFullyQualified(path) && Uri.TryCreate(path, UriKind.Absolute, out _);
|
||||
}
|
||||
}
|
||||
88
src/TED/TED.Utils/ImageUtilities.cs
Normal file
88
src/TED/TED.Utils/ImageUtilities.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace TED.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides utility methods for working with images
|
||||
/// </summary>
|
||||
public class ImageUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Scales the image dimensions to fit within the specified maximum width and height, maintaining the original aspect ratio.
|
||||
/// </summary>
|
||||
/// <param name="srcWidth">The original image width.</param>
|
||||
/// <param name="srcHeight">The original image height.</param>
|
||||
/// <param name="maxWidth">The maximum allowable width for the scaled image.</param>
|
||||
/// <param name="maxHeight">The maximum allowable height for the scaled image.</param>
|
||||
/// <param name="destWidth">The calculated width for the scaled image.</param>
|
||||
/// <param name="destHeight">The calculated height for the scaled image.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the perceived luminance of the current desktop wallpaper.
|
||||
/// </summary>
|
||||
/// <returns>A value representing the calculated perceived luminance of the wallpaper. Returns 0.0 if the wallpaper path is not found.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the perceived luminance of a Bitmap image.
|
||||
/// </summary>
|
||||
/// <param name="bm">The Bitmap image to calculate the luminance for.</param>
|
||||
/// <returns>A value representing the calculated luminance of the image.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
45
src/TED/TED.Utils/SystemUtilities.cs
Normal file
45
src/TED/TED.Utils/SystemUtilities.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using Microsoft.Win32;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace TED.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides utility functions related to system configurations.
|
||||
/// </summary>
|
||||
public class SystemUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Get's the Rectangle of the Primary Screen in Virtual Screen space.
|
||||
/// </summary>
|
||||
/// <returns>The Rectangle of the Primary Screen in Virtual Screen space.</returns>
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the current wallpaper path from the system registry.
|
||||
/// </summary>
|
||||
/// <returns>The full path of the current desktop wallpaper.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/TED/TED.Utils/Tokenizer.cs
Normal file
41
src/TED/TED.Utils/Tokenizer.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace TED.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides functionality for replacing tokens in a string.
|
||||
/// </summary>
|
||||
internal class Tokenizer
|
||||
{
|
||||
/// <summary>
|
||||
/// A dictionary that maps token names to their corresponding values.
|
||||
/// </summary>
|
||||
static Dictionary<string, Func<string>> TokenLookup = new Dictionary<string, Func<string>>()
|
||||
{
|
||||
{ "@userName", () => WindowsIdentity.GetCurrent().Name },
|
||||
{ "@machineName", () => Environment.MachineName },
|
||||
{ "@os", () => Environment.OSVersion.ToString() },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Replaces tokens in the input string with their corresponding values.
|
||||
/// </summary>
|
||||
/// <param name="input">The string that may contain tokens to be replaced.</param>
|
||||
/// <returns>The input string with all tokens replaced by their corresponding values.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
169
src/TED/TED.Utils/Win32Native.cs
Normal file
169
src/TED/TED.Utils/Win32Native.cs
Normal file
@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace TED.Utils
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// The information and methods stored within this class are largely attributed to <see href="https://www.pinvoke.net/"/>
|
||||
/// It has been a fantastic resource for tinkering with and learning how native methods work.
|
||||
///
|
||||
/// </summary>
|
||||
|
||||
public class Win32Native
|
||||
{
|
||||
public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
|
||||
|
||||
[Flags()]
|
||||
public enum DeviceContextValues : uint
|
||||
{
|
||||
/// <summary>DCX_WINDOW: Returns a DC that corresponds to the window rectangle rather
|
||||
/// than the client rectangle.</summary>
|
||||
Window = 0x00000001,
|
||||
|
||||
/// <summary>DCX_CACHE: Returns a DC from the cache, rather than the OWNDC or CLASSDC
|
||||
/// window. Essentially overrides CS_OWNDC and CS_CLASSDC.</summary>
|
||||
Cache = 0x00000002,
|
||||
|
||||
/// <summary>DCX_NORESETATTRS: Does not reset the attributes of this DC to the
|
||||
/// default attributes when this DC is released.</summary>
|
||||
NoResetAttrs = 0x00000004,
|
||||
|
||||
/// <summary>DCX_CLIPCHILDREN: Excludes the visible regions of all child windows
|
||||
/// below the window identified by hWnd.</summary>
|
||||
ClipChildren = 0x00000008,
|
||||
|
||||
/// <summary>DCX_CLIPSIBLINGS: Excludes the visible regions of all sibling windows
|
||||
/// above the window identified by hWnd.</summary>
|
||||
ClipSiblings = 0x00000010,
|
||||
|
||||
/// <summary>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.</summary>
|
||||
ParentClip = 0x00000020,
|
||||
|
||||
/// <summary>DCX_EXCLUDERGN: The clipping region identified by hrgnClip is excluded
|
||||
/// from the visible region of the returned DC.</summary>
|
||||
ExcludeRgn = 0x00000040,
|
||||
|
||||
/// <summary>DCX_INTERSECTRGN: The clipping region identified by hrgnClip is
|
||||
/// intersected with the visible region of the returned DC.</summary>
|
||||
IntersectRgn = 0x00000080,
|
||||
|
||||
/// <summary>DCX_EXCLUDEUPDATE: Unknown...Undocumented</summary>
|
||||
ExcludeUpdate = 0x00000100,
|
||||
|
||||
/// <summary>DCX_INTERSECTUPDATE: Unknown...Undocumented</summary>
|
||||
IntersectUpdate = 0x00000200,
|
||||
|
||||
/// <summary>DCX_LOCKWINDOWUPDATE: Allows drawing even if there is a LockWindowUpdate
|
||||
/// call in effect that would otherwise exclude this window. Used for drawing during
|
||||
/// tracking.</summary>
|
||||
LockWindowUpdate = 0x00000400,
|
||||
|
||||
/// <summary>DCX_USESTYLE: Undocumented, something related to WM_NCPAINT message.</summary>
|
||||
UseStyle = 0x00010000,
|
||||
|
||||
/// <summary>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.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
40
src/TED/TED.csproj
Normal file
40
src/TED/TED.csproj
Normal file
@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0-windows10.0.22621.0</TargetFramework>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>Link</TrimMode>
|
||||
<RootNamespace>TED</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<Configurations>Debug;Release;Open</Configurations>
|
||||
<Version>1.0.4.1-RC1</Version>
|
||||
<InformationalVersion>Prerelease</InformationalVersion>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<Company>Health IT</Company>
|
||||
<Title>TED - Tag Every Desktop</Title>
|
||||
<Authors>Health IT</Authors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Open|AnyCPU'">
|
||||
<Optimize>True</Optimize>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<Optimize>False</Optimize>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WindowsForms" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\PublishProfiles\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
17
src/TED/app.manifest
Normal file
17
src/TED/app.manifest
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
Loading…
Reference in New Issue
Block a user