[ColorPicker] CMYK, HSV and HSL color format (#6975)

* Add HSL and HSV color formats + cleanup + docu

* Fix build problem (lang version)

* Add CYMK color + replace float with double values

* ups - fix cmyk text format

* fix wrong settings text + doc typo fix

* Address feedback

* Address feedback + fix to small window size

* adress feedback + more cleanup

* typo fix

* Avoid possible division by zero + unit test

* Address feedback - move all represenation to own helper class + UnitTest

* Address feedback -> switch to mstest framework
This commit is contained in:
Tobias Sekan
2020-10-21 20:09:30 +02:00
committed by GitHub
parent cd8c9c5375
commit 73df7b5deb
19 changed files with 676 additions and 144 deletions

View File

@@ -269,6 +269,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Calculator
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Folder.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.Folder.UnitTests\Microsoft.Plugin.Folder.UnitTests.csproj", "{4FA206A5-F69F-4193-BF8F-F6EEB496734C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest-ColorPickerUI", "UnitTest-ColorPickerUI\UnitTest-ColorPickerUI.csproj", "{090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "logging", "src\logging\logging.vcxproj", "{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}"
EndProject
Global
@@ -545,6 +547,10 @@ Global
{4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Debug|x64.Build.0 = Debug|x64
{4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|x64.ActiveCfg = Release|x64
{4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|x64.Build.0 = Release|x64
{090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Debug|x64.ActiveCfg = Debug|Any CPU
{090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Debug|x64.Build.0 = Debug|Any CPU
{090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Release|x64.ActiveCfg = Release|x64
{090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Release|x64.Build.0 = Release|x64
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.ActiveCfg = Debug|x64
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.Build.0 = Debug|x64
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.ActiveCfg = Release|x64
@@ -627,6 +633,7 @@ Global
{0F85E674-34AE-443D-954C-8321EB8B93B1} = {C3081D9A-1586-441A-B5F4-ED815B3719C1}
{632BBE62-5421-49EA-835A-7FFA4F499BD6} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
{4FA206A5-F69F-4193-BF8F-F6EEB496734C} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
{090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1} = {1D78B84B-CA39-406C-98F4-71F7EC266CC0}
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F} = {1AFB6476-670D-4E80-A464-657E01DFF482}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

View File

@@ -0,0 +1,187 @@
using System;
using System.Drawing;
using ColorPicker.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTest_ColorPickerUI.Helpers
{
/// <summary>
/// Test class to test <see cref="ColorConverter"/>
/// </summary>
[TestClass]
public class ColorConverterTest
{
// test values taken from https://de.wikipedia.org/wiki/HSV-Farbraum
[TestMethod]
[DataRow(000, 000, 000, 000, 000, 000)] // Black
[DataRow(000, 000, 100, 100, 100, 100)] // White
[DataRow(000, 100, 050, 100, 000, 000)] // Red
[DataRow(015, 100, 050, 100, 025, 000)] // Vermilion/Cinnabar
[DataRow(020, 060, 022.5, 036, 018, 009)] // Brown
[DataRow(030, 100, 050, 100, 050, 000)] // Orange
[DataRow(045, 100, 050, 100, 075, 000)] // Saffron
[DataRow(060, 100, 050, 100, 100, 000)] // Yellow
[DataRow(075, 100, 050, 075, 100, 000)] // Light green-yellow
[DataRow(090, 100, 050, 050, 100, 000)] // Green-yellow
[DataRow(105, 100, 050, 025, 100, 000)] // Lime
[DataRow(120, 100, 025, 000, 050, 000)] // Dark green
[DataRow(120, 100, 050, 000, 100, 000)] // Green
[DataRow(135, 100, 050, 000, 100, 025)] // Light blue-green
[DataRow(150, 100, 050, 000, 100, 050)] // Blue-green
[DataRow(165, 100, 050, 000, 100, 075)] // Green-cyan
[DataRow(180, 100, 050, 000, 100, 100)] // Cyan
[DataRow(195, 100, 050, 000, 075, 100)] // Blue-cyan
[DataRow(210, 100, 050, 000, 050, 100)] // Green-blue
[DataRow(225, 100, 050, 000, 025, 100)] // Light green-blue
[DataRow(240, 100, 050, 000, 000, 100)] // Blue
[DataRow(255, 100, 050, 025, 000, 100)] // Indigo
[DataRow(270, 100, 050, 050, 000, 100)] // Purple
[DataRow(285, 100, 050, 075, 000, 100)] // Blue-magenta
[DataRow(300, 100, 050, 100, 000, 100)] // Magenta
[DataRow(315, 100, 050, 100, 000, 075)] // Red-magenta
[DataRow(330, 100, 050, 100, 000, 050)] // Blue-red
[DataRow(345, 100, 050, 100, 000, 025)] // Light blue-red
public void ColorRGBtoHSL(double hue, double saturation, double lightness, int red, int green, int blue)
{
red = Convert.ToInt32(Math.Round(255d / 100d * red)); // [0%..100%] to [0..255]
green = Convert.ToInt32(Math.Round(255d / 100d * green)); // [0%..100%] to [0..255]
blue = Convert.ToInt32(Math.Round(255d / 100d * blue)); // [0%..100%] to [0..255]
var color = Color.FromArgb(255, red, green, blue);
var result = ColorHelper.ConvertToHSLColor(color);
// hue[0<>..360<EFBFBD>]
Assert.AreEqual(result.hue, hue, 0.2d);
// saturation[0..1]
Assert.AreEqual(result.saturation * 100d, saturation, 0.2d);
// lightness[0..1]
Assert.AreEqual(result.lightness * 100d, lightness, 0.2d);
}
// test values taken from https://de.wikipedia.org/wiki/HSV-Farbraum
[TestMethod]
[DataRow(000, 000, 000, 000, 000, 000)] // Black
[DataRow(000, 000, 100, 100, 100, 100)] // White
[DataRow(000, 100, 100, 100, 000, 000)] // Red
[DataRow(015, 100, 100, 100, 025, 000)] // Vermilion/Cinnabar
[DataRow(020, 075, 036, 036, 018, 009)] // Brown
[DataRow(030, 100, 100, 100, 050, 000)] // Orange
[DataRow(045, 100, 100, 100, 075, 000)] // Saffron
[DataRow(060, 100, 100, 100, 100, 000)] // Yellow
[DataRow(075, 100, 100, 075, 100, 000)] // Light green-yellow
[DataRow(090, 100, 100, 050, 100, 000)] // Green-yellow
[DataRow(105, 100, 100, 025, 100, 000)] // Lime
[DataRow(120, 100, 050, 000, 050, 000)] // Dark green
[DataRow(120, 100, 100, 000, 100, 000)] // Green
[DataRow(135, 100, 100, 000, 100, 025)] // Light blue-green
[DataRow(150, 100, 100, 000, 100, 050)] // Blue-green
[DataRow(165, 100, 100, 000, 100, 075)] // Green-cyan
[DataRow(180, 100, 100, 000, 100, 100)] // Cyan
[DataRow(195, 100, 100, 000, 075, 100)] // Blue-cyan
[DataRow(210, 100, 100, 000, 050, 100)] // Green-blue
[DataRow(225, 100, 100, 000, 025, 100)] // Light green-blue
[DataRow(240, 100, 100, 000, 000, 100)] // Blue
[DataRow(255, 100, 100, 025, 000, 100)] // Indigo
[DataRow(270, 100, 100, 050, 000, 100)] // Purple
[DataRow(285, 100, 100, 075, 000, 100)] // Blue-magenta
[DataRow(300, 100, 100, 100, 000, 100)] // Magenta
[DataRow(315, 100, 100, 100, 000, 075)] // Red-magenta
[DataRow(330, 100, 100, 100, 000, 050)] // Blue-red
[DataRow(345, 100, 100, 100, 000, 025)] // Light blue-red
public void ColorRGBtoHSV(double hue, double saturation, double value, int red, int green, int blue)
{
red = Convert.ToInt32(Math.Round(255d / 100d * red)); // [0%..100%] to [0..255]
green = Convert.ToInt32(Math.Round(255d / 100d * green)); // [0%..100%] to [0..255]
blue = Convert.ToInt32(Math.Round(255d / 100d * blue)); // [0%..100%] to [0..255]
var color = Color.FromArgb(255, red, green, blue);
var result = ColorHelper.ConvertToHSVColor(color);
// hue [0<>..360<EFBFBD>]
Assert.AreEqual(result.hue, hue, 0.2d);
// saturation[0..1]
Assert.AreEqual(result.saturation * 100d, saturation, 0.2d);
// value[0..1]
Assert.AreEqual(result.value * 100d, value, 0.2d);
}
[TestMethod]
[DataRow(000, 000, 000, 100, 000, 000, 000)] // Black
[DataRow(000, 000, 000, 000, 255, 255, 255)] // White
[DataRow(000, 100, 100, 000, 255, 000, 000)] // Red
[DataRow(000, 075, 100, 000, 255, 064, 000)] // Vermilion/Cinnabar
[DataRow(000, 050, 075, 064, 092, 046, 023)] // Brown
[DataRow(000, 050, 100, 000, 255, 128, 000)] // Orange
[DataRow(000, 025, 100, 000, 255, 192, 000)] // Saffron
[DataRow(000, 000, 100, 000, 255, 255, 000)] // Yellow
[DataRow(025, 000, 100, 000, 192, 255, 000)] // Light green-yellow
[DataRow(050, 000, 100, 000, 128, 255, 000)] // Green-yellow
[DataRow(075, 000, 100, 000, 064, 255, 000)] // Lime
[DataRow(100, 000, 100, 050, 000, 128, 000)] // Dark green
[DataRow(100, 000, 100, 000, 000, 255, 000)] // Green
[DataRow(100, 000, 075, 000, 000, 255, 064)] // Light blue-green
[DataRow(100, 000, 050, 000, 000, 255, 128)] // Blue-green
[DataRow(100, 000, 025, 000, 000, 255, 192)] // Green-cyan
[DataRow(100, 000, 000, 000, 000, 255, 255)] // Cyan
[DataRow(100, 025, 000, 000, 000, 192, 255)] // Blue-cyan
[DataRow(100, 050, 000, 000, 000, 128, 255)] // Green-blue
[DataRow(100, 075, 000, 000, 000, 064, 255)] // Light green-blue
[DataRow(100, 100, 000, 000, 000, 000, 255)] // Blue
[DataRow(075, 100, 000, 000, 064, 000, 255)] // Indigo
[DataRow(050, 100, 000, 000, 128, 000, 255)] // Purple
[DataRow(025, 100, 000, 000, 192, 000, 255)] // Blue-magenta
[DataRow(000, 100, 000, 000, 255, 000, 255)] // Magenta
[DataRow(000, 100, 025, 000, 255, 000, 192)] // Red-magenta
[DataRow(000, 100, 050, 000, 255, 000, 128)] // Blue-red
[DataRow(000, 100, 075, 000, 255, 000, 064)] // Light blue-red
public void ColorRGBtoCMYK(int cyan, int magenta, int yellow, int blackKey, int red, int green, int blue)
{
var color = Color.FromArgb(255, red, green, blue);
var result = ColorHelper.ConvertToCMYKColor(color);
// cyan[0..1]
Assert.AreEqual(result.cyan * 100d, cyan, 0.5d);
// magenta[0..1]
Assert.AreEqual(result.magenta * 100d, magenta, 0.5d);
// yellow[0..1]
Assert.AreEqual(result.yellow * 100d, yellow, 0.5d);
// black[0..1]
Assert.AreEqual(result.blackKey * 100d, blackKey, 0.5d);
}
[TestMethod]
public void ColorRGBtoCMYKZeroDiv()
{
for(var red = 0; red < 256; red++)
{
for(var blue = 0; blue < 256; blue++)
{
for(var green = 0; green < 256; green++)
{
var color = Color.FromArgb(red, green, blue);
Exception? exception = null;
try
{
_ = ColorHelper.ConvertToCMYKColor(color);
}
catch(Exception ex)
{
exception = ex;
}
Assert.IsNull(exception);
}
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
using ColorPicker.Helpers;
using Microsoft.PowerToys.Settings.UI.Lib;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Drawing;
namespace UnitTest_ColorPickerUI.Helpers
{
[TestClass]
public class ColorRepresentationHelperTest
{
[TestMethod]
[DataRow(ColorRepresentationType.CMYK, "cmyk(0%, 0%, 0%, 100%)")]
[DataRow(ColorRepresentationType.HEX, "#000000")]
[DataRow(ColorRepresentationType.HSL, "hsl(0, 0%, 0%)")]
[DataRow(ColorRepresentationType.HSV, "hsv(0, 0%, 0%)")]
[DataRow(ColorRepresentationType.RGB, "rgb(0, 0, 0)")]
public void ColorRGBtoCMYKZeroDiv(ColorRepresentationType type, string expected)
{
var result = ColorRepresentationHelper.GetStringRepresentation(Color.Black, type);
Assert.AreEqual(result, expected);
}
}
}

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>UnitTest_ColorPickerUI</RootNamespace>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<LangVersion>9.0</LangVersion>
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\core\Microsoft.PowerToys.Settings.UI.Lib\Microsoft.PowerToys.Settings.UI.Lib.csproj" />
<ProjectReference Include="..\src\modules\colorPicker\ColorPickerUI\ColorPickerUI.csproj" />
</ItemGroup>
</Project>

View File

@@ -9,8 +9,30 @@ namespace Microsoft.PowerToys.Settings.UI.Lib
{
public enum ColorRepresentationType
{
/// <summary>
/// Color presentation as hexadecimal color value without the alpha-value (e.g. #0055FF)
/// </summary>
HEX = 0,
/// <summary>
/// Color presentation as RGB color value (red[0..255], green[0..255], blue[0..255])
/// </summary>
RGB = 1,
/// <summary>
/// Color presentation as CMYK color value (cyan[0%..100%], magenta[0%..100%], yellow[0%..100%], black key[0%..100%])
/// </summary>
CMYK = 2,
/// <summary>
/// Color presentation as HSL color value (hue[0°..360°], saturation[0..100%], lightness[0%..100%])
/// </summary>
HSL = 3,
/// <summary>
/// Color presentation as HSV color value (hue[0°..360°], saturation[0%..100%], value[0%..100%])
/// </summary>
HSV = 4,
}
public class ColorPickerProperties
@@ -31,8 +53,6 @@ namespace Microsoft.PowerToys.Settings.UI.Lib
public ColorRepresentationType CopiedColorRepresentation { get; set; }
public override string ToString()
{
return JsonSerializer.Serialize(this);
}
=> JsonSerializer.Serialize(this);
}
}

View File

@@ -68,7 +68,10 @@
Width="240"
IsEnabled="{Binding IsEnabled}">
<ComboBoxItem Content="HEX - #FFAA00"/>
<ComboBoxItem Content="RGB - RGB(100, 50, 75)"/>
<ComboBoxItem Content="RGB - rgb(100, 50, 75)"/>
<ComboBoxItem Content="CMYK - cmyk(100%, 50%, 75%, 0%)"/>
<ComboBoxItem Content="HSL - hsl(100, 50%, 75%)"/>
<ComboBoxItem Content="HSV - hsv(100, 50%, 75%)"/>
</ComboBox>
<!--

View File

@@ -6,6 +6,7 @@ using System;
using System.Windows;
using System.Windows.Interactivity;
using System.Windows.Media.Animation;
using ColorPicker.Constants;
namespace ColorPicker.Behaviors
{
@@ -37,21 +38,31 @@ namespace ColorPicker.Behaviors
private void Appear()
{
var opacityAppear = new DoubleAnimation(0, 1.0, new Duration(TimeSpan.FromMilliseconds(250)));
opacityAppear.EasingFunction = new QuadraticEase() { EasingMode = EasingMode.EaseOut };
var duration = new Duration(TimeSpan.FromMilliseconds(250));
var resize = new DoubleAnimation(0, 180, new Duration(TimeSpan.FromMilliseconds(250)));
resize.EasingFunction = new ExponentialEase() { EasingMode = EasingMode.EaseOut };
AssociatedObject.BeginAnimation(Window.OpacityProperty, opacityAppear);
AssociatedObject.BeginAnimation(Window.WidthProperty, resize);
var opacityAppear = new DoubleAnimation(0d, 1d, duration)
{
EasingFunction = new QuadraticEase() { EasingMode = EasingMode.EaseOut },
};
var resize = new DoubleAnimation(0d, WindowConstant.PickerWindowWidth, duration)
{
EasingFunction = new ExponentialEase() { EasingMode = EasingMode.EaseOut },
};
AssociatedObject.BeginAnimation(UIElement.OpacityProperty, opacityAppear);
AssociatedObject.BeginAnimation(FrameworkElement.WidthProperty, resize);
}
private void Hide()
{
var opacityAppear = new DoubleAnimation(0, new Duration(TimeSpan.FromMilliseconds(1)));
var resize = new DoubleAnimation(0, new Duration(TimeSpan.FromMilliseconds(1)));
AssociatedObject.BeginAnimation(Window.OpacityProperty, opacityAppear);
AssociatedObject.BeginAnimation(Window.WidthProperty, resize);
var duration = new Duration(TimeSpan.FromMilliseconds(1));
var opacityAppear = new DoubleAnimation(0d, duration);
var resize = new DoubleAnimation(0d, duration);
AssociatedObject.BeginAnimation(UIElement.OpacityProperty, opacityAppear);
AssociatedObject.BeginAnimation(FrameworkElement.WidthProperty, resize);
}
}
}

View File

@@ -4,6 +4,9 @@
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("UnitTest-ColorPickerUI")]
namespace ColorPicker
{
@@ -21,8 +24,6 @@ namespace ColorPicker
}
public static void Dispose()
{
Container.Dispose();
}
=> Container.Dispose();
}
}

View File

@@ -62,7 +62,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<LangVersion>9.0</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
@@ -73,7 +73,7 @@
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<LangVersion>9.0</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
@@ -107,6 +107,9 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Behaviors\GridEffectBehavior.cs" />
<Compile Include="Constants\WindowConstant.cs" />
<Compile Include="Helpers\ColorHelper.cs" />
<Compile Include="Helpers\ColorRepresentationHelper.cs" />
<Compile Include="Helpers\IThrottledActionInvoker.cs" />
<Compile Include="Helpers\ThrottledActionInvoker.cs" />
<Compile Include="NativeMethods.cs" />

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ColorPicker.Constants
{
/// <summary>
/// This class contains all assembly wide constants for windows and views
/// </summary>
public static class WindowConstant
{
/// <summary>
/// The width of the color picker window
/// </summary>
public const double PickerWindowWidth = 240d;
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
namespace ColorPicker.Helpers
{
/// <summary>
/// Helper class to easier work with colors
/// </summary>
internal static class ColorHelper
{
/// <summary>
/// Convert a given <see cref="Color"/> color to a HSL color (hue, saturation, lightness)
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The hue [0°..360°], saturation [0..1] and lightness [0..1] values of the converted color</returns>
internal static (double hue, double saturation, double lightness) ConvertToHSLColor(Color color)
{
var min = Math.Min(Math.Min(color.R, color.G), color.B) / 255d;
var max = Math.Max(Math.Max(color.R, color.G), color.B) / 255d;
var lightness = (max + min) / 2d;
if (lightness == 0d || min == max)
{
return (color.GetHue(), 0d, lightness);
}
else if (lightness is > 0d and <= 0.5d)
{
return (color.GetHue(), (max - min) / (max + min), lightness);
}
return (color.GetHue(), (max - min) / (2d - (max + min)), lightness);
}
/// <summary>
/// Convert a given <see cref="Color"/> color to a HSV color (hue, saturation, value)
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The hue [0°..360°], saturation [0..1] and value [0..1] of the converted color</returns>
internal static (double hue, double saturation, double value) ConvertToHSVColor(Color color)
{
var min = Math.Min(Math.Min(color.R, color.G), color.B) / 255d;
var max = Math.Max(Math.Max(color.R, color.G), color.B) / 255d;
return (color.GetHue(), max == 0d ? 0d : (max - min) / max, max);
}
/// <summary>
/// Convert a given <see cref="Color"/> color to a CYMK color (cyan, magenta, yellow, black key)
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The cyan[0..1], magenta[0..1], yellow[0..1] and black key[0..1] of the converted color</returns>
internal static (double cyan, double magenta, double yellow, double blackKey) ConvertToCMYKColor(Color color)
{
// special case for black (avoid division by zero)
if (color.R == 0 && color.G == 0 && color.B == 0)
{
return (0d, 0d, 0d, 1d);
}
var red = color.R / 255d;
var green = color.G / 255d;
var blue = color.B / 255d;
var blackKey = 1d - Math.Max(Math.Max(red, green), blue);
// special case for black (avoid division by zero)
if (1d - blackKey == 0d)
{
return (0d, 0d, 0d, 1d);
}
var cyan = (1d - red - blackKey) / (1d - blackKey);
var magenta = (1d - green - blackKey) / (1d - blackKey);
var yellow = (1d - blue - blackKey) / (1d - blackKey);
return (cyan, magenta, yellow, blackKey);
}
}
}

View File

@@ -0,0 +1,112 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using System.Globalization;
using Microsoft.PowerToys.Settings.UI.Lib;
namespace ColorPicker.Helpers
{
/// <summary>
/// Helper class to easier work with color represantation
/// </summary>
internal static class ColorRepresentationHelper
{
/// <summary>
/// Return a <see cref="string"/> representation of a given <see cref="Color"/>
/// </summary>
/// <param name="color">The <see cref="Color"/> for the presentation</param>
/// <param name="colorRepresentationType">The type of the representation</param>
/// <returns>A <see cref="string"/> representation of a color</returns>
internal static string GetStringRepresentation(Color color, ColorRepresentationType colorRepresentationType)
=> colorRepresentationType switch
{
ColorRepresentationType.CMYK => ColorToCYMK(color),
ColorRepresentationType.HEX => ColorToHex(color),
ColorRepresentationType.HSL => ColorToHSL(color),
ColorRepresentationType.HSV => ColorToHSV(color),
ColorRepresentationType.RGB => ColorToRGB(color),
// Fall-back value, when "_userSettings.CopiedColorRepresentation.Value" is incorrect
_ => ColorToHex(color),
};
/// <summary>
/// Return a hexadecimal <see cref="string"/> representation of a RGB color
/// </summary>
/// <param name="color">The see cref="Color"/> for the hexadecimal presentation</param>
/// <returns>A hexadecimal <see cref="string"/> representation of a RGB color</returns>
private static string ColorToHex(Color color)
=> $"#{color.R.ToString("X2", CultureInfo.InvariantCulture)}"
+ $"{color.G.ToString("X2", CultureInfo.InvariantCulture)}"
+ $"{color.B.ToString("X2", CultureInfo.InvariantCulture)}";
/// <summary>
/// Return a <see cref="string"/> representation of a RGB color
/// </summary>
/// <param name="color">The see cref="Color"/> for the RGB color presentation</param>
/// <returns>A <see cref="string"/> representation of a RGB color</returns>
private static string ColorToRGB(Color color)
=> $"rgb({color.R.ToString(CultureInfo.InvariantCulture)}"
+ $", {color.G.ToString(CultureInfo.InvariantCulture)}"
+ $", {color.B.ToString(CultureInfo.InvariantCulture)})";
/// <summary>
/// Return a <see cref="string"/> representation of a HSL color
/// </summary>
/// <param name="color">The see cref="Color"/> for the HSL color presentation</param>
/// <returns>A <see cref="string"/> representation of a HSL color</returns>
private static string ColorToHSL(Color color)
{
var (hue, saturation, lightness) = ColorHelper.ConvertToHSLColor(color);
hue = Math.Round(hue);
saturation = Math.Round(saturation * 100);
lightness = Math.Round(lightness * 100);
return $"hsl({hue.ToString(CultureInfo.InvariantCulture)}"
+ $", {saturation.ToString(CultureInfo.InvariantCulture)}%"
+ $", {lightness.ToString(CultureInfo.InvariantCulture)}%)";
}
/// <summary>
/// Return a <see cref="string"/> representation of a HSV color
/// </summary>
/// <param name="color">The see cref="Color"/> for the HSV color presentation</param>
/// <returns>A <see cref="string"/> representation of a HSV color</returns>
private static string ColorToHSV(Color color)
{
var (hue, saturation, value) = ColorHelper.ConvertToHSVColor(color);
hue = Math.Round(hue);
saturation = Math.Round(saturation * 100);
value = Math.Round(value * 100);
return $"hsv({hue.ToString(CultureInfo.InvariantCulture)}"
+ $", {saturation.ToString(CultureInfo.InvariantCulture)}%"
+ $", {value.ToString(CultureInfo.InvariantCulture)}%)";
}
/// <summary>
/// Return a <see cref="string"/> representation of a HSV color
/// </summary>
/// <param name="color">The see cref="Color"/> for the HSV color presentation</param>
/// <returns>A <see cref="string"/> representation of a HSV color</returns>
private static string ColorToCYMK(Color color)
{
var (cyan, magenta, yellow, blackKey) = ColorHelper.ConvertToCMYKColor(color);
cyan = Math.Round(cyan * 100);
magenta = Math.Round(magenta * 100);
yellow = Math.Round(yellow * 100);
blackKey = Math.Round(blackKey * 100);
return $"cmyk({cyan.ToString(CultureInfo.InvariantCulture)}%"
+ $", {magenta.ToString(CultureInfo.InvariantCulture)}%"
+ $", {yellow.ToString(CultureInfo.InvariantCulture)}%"
+ $", {blackKey.ToString(CultureInfo.InvariantCulture)}%)";
}
}
}

View File

@@ -3,11 +3,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ColorPickerUI"
xmlns:constants="clr-namespace:ColorPicker.Constants"
mc:Ignorable="d"
xmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:behaviors="clr-namespace:ColorPicker.Behaviors"
Title="Color Picker" Height="50" Width="180" WindowStyle="None" Opacity="0.01" ShowInTaskbar="False" ResizeMode="NoResize" Topmost="True" Background="Transparent" AllowsTransparency="True">
Title="Color Picker" Height="50" Width="{x:Static constants:WindowConstant.PickerWindowWidth}" WindowStyle="None" Opacity="0.01" ShowInTaskbar="False" ResizeMode="NoResize" Topmost="True" Background="Transparent" AllowsTransparency="True">
<e:Interaction.Behaviors>
<behaviors:ChangeWindowPositionBehavior/>
<behaviors:AppearAnimationBehavior/>

View File

@@ -8,10 +8,14 @@ namespace ColorPicker.ViewModelContracts
{
public interface IMainViewModel
{
string HexColor { get; }
string RgbColor { get; }
/// <summary>
/// Gets the text representation of the selected color value
/// </summary>
string ColorText { get; }
/// <summary>
/// Gets the current selected color as a <see cref="Brush"/>
/// </summary>
Brush ColorBrush { get; }
}
}

View File

@@ -4,8 +4,8 @@
using System;
using System.ComponentModel.Composition;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using ColorPicker.Common;
@@ -23,11 +23,23 @@ namespace ColorPicker.ViewModels
[Export(typeof(IMainViewModel))]
public class MainViewModel : ViewModelBase, IMainViewModel
{
/// <summary>
/// Defined error code for "clipboard can't open"
/// </summary>
private const uint ErrorCodeClipboardCantOpen = 0x800401D0;
private readonly ZoomWindowHelper _zoomWindowHelper;
private readonly AppStateHandler _appStateHandler;
private readonly IUserSettings _userSettings;
private string _hexColor;
private string _rgbColor;
/// <summary>
/// Backing field for <see cref="OtherColor"/>
/// </summary>
private string _colorText;
/// <summary>
/// Backing field for <see cref="ColorBrush"/>
/// </summary>
private Brush _colorBrush;
[ImportingConstructor]
@@ -52,41 +64,12 @@ namespace ColorPicker.ViewModels
keyboardMonitor?.Start();
}
public string HexColor
{
get
{
return _hexColor;
}
private set
{
_hexColor = value;
OnPropertyChanged();
}
}
public string RgbColor
{
get
{
return _rgbColor;
}
private set
{
_rgbColor = value;
OnPropertyChanged();
}
}
/// <summary>
/// Gets the current selected color as a <see cref="Brush"/>
/// </summary>
public Brush ColorBrush
{
get
{
return _colorBrush;
}
get => _colorBrush;
private set
{
_colorBrush = value;
@@ -94,74 +77,80 @@ namespace ColorPicker.ViewModels
}
}
private void Mouse_ColorChanged(object sender, System.Drawing.Color color)
/// <summary>
/// Gets the text representation of the selected color value
/// </summary>
public string ColorText
{
HexColor = ColorToHex(color);
RgbColor = ColorToRGB(color);
ColorBrush = new SolidColorBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
get => _colorText;
private set
{
_colorText = value;
OnPropertyChanged();
}
}
/// <summary>
/// Tell the color picker that the color on the position of the mouse cursor have changed
/// </summary>
/// <param name="sender">The sender of this event</param>
/// <param name="color">The new <see cref="Color"/> under the mouse cursor</param>
private void Mouse_ColorChanged(object sender, System.Drawing.Color color)
{
ColorBrush = new SolidColorBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
ColorText = ColorRepresentationHelper.GetStringRepresentation(color, _userSettings.CopiedColorRepresentation.Value);
}
/// <summary>
/// Tell the color picker that the user have press a mouse button (after release the button)
/// </summary>
/// <param name="sender">The sender of this event</param>
/// <param name="p">The current <see cref="System.Drawing.Point"/> of the mouse cursor</param>
private void MouseInfoProvider_OnMouseDown(object sender, System.Drawing.Point p)
{
string colorRepresentationToCopy = string.Empty;
switch (_userSettings.CopiedColorRepresentation.Value)
{
case ColorRepresentationType.HEX:
colorRepresentationToCopy = HexColor;
break;
case ColorRepresentationType.RGB:
colorRepresentationToCopy = RgbColor;
break;
default:
break;
}
CopyToClipboard(colorRepresentationToCopy);
CopyToClipboard(ColorText);
_appStateHandler.HideColorPicker();
PowerToysTelemetry.Log.WriteEvent(new ColorPickerShowEvent());
}
private static void CopyToClipboard(string colorRepresentationToCopy)
/// <summary>
/// Copy the given text to the Windows clipboard
/// </summary>
/// <param name="text">The text to copy to the Windows clipboard</param>
private static void CopyToClipboard(string text)
{
if (!string.IsNullOrEmpty(colorRepresentationToCopy))
if (string.IsNullOrEmpty(text))
{
// nasty hack - sometimes clipboard can be in use and it will raise and exception
for (int i = 0; i < 10; i++)
{
try
{
Clipboard.SetText(colorRepresentationToCopy);
break;
}
catch (COMException ex)
{
const uint CLIPBRD_E_CANT_OPEN = 0x800401D0;
if ((uint)ex.ErrorCode != CLIPBRD_E_CANT_OPEN)
{
Logger.LogError("Failed to set text into clipboard", ex);
}
}
return;
}
System.Threading.Thread.Sleep(10);
// nasty hack - sometimes clipboard can be in use and it will raise and exception
for (var i = 0; i < 10; i++)
{
try
{
Clipboard.SetText(text);
break;
}
catch (COMException ex)
{
if ((uint)ex.ErrorCode != ErrorCodeClipboardCantOpen)
{
Logger.LogError("Failed to set text into clipboard", ex);
}
}
Thread.Sleep(10);
}
}
/// <summary>
/// Tell the color picker that the user have used the mouse wheel
/// </summary>
/// <param name="sender">The sender of this event</param>
/// <param name="e">The new values for the zoom</param>
private void MouseInfoProvider_OnMouseWheel(object sender, Tuple<Point, bool> e)
{
_zoomWindowHelper.Zoom(e.Item1, e.Item2);
}
private static string ColorToHex(System.Drawing.Color c)
{
return "#" + c.R.ToString("X2", CultureInfo.InvariantCulture) + c.G.ToString("X2", CultureInfo.InvariantCulture) + c.B.ToString("X2", CultureInfo.InvariantCulture);
}
private static string ColorToRGB(System.Drawing.Color c)
{
return "RGB(" + c.R.ToString(CultureInfo.InvariantCulture) + "," + c.G.ToString(CultureInfo.InvariantCulture) + "," + c.B.ToString(CultureInfo.InvariantCulture) + ")";
}
=> _zoomWindowHelper.Zoom(e.Item1, e.Item2);
}
}

View File

@@ -1,31 +1,55 @@
<UserControl x:Class="ColorPicker.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ColorPicker.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModel="clr-namespace:ColorPicker.ViewModels"
mc:Ignorable="d"
d:DesignHeight="50" d:DesignWidth="180"
d:DataContext="{d:DesignInstance viewModel:MainViewModel, IsDesignTimeCreatable=True}">
xmlns:constants="clr-namespace:ColorPicker.Constants"
mc:Ignorable="d"
d:DesignHeight="50"
d:DesignWidth="{x:Static constants:WindowConstant.PickerWindowWidth}"
d:DataContext="{d:DesignInstance viewModel:MainViewModel, IsDesignTimeCreatable=True}" >
<Grid Background="Transparent" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="50" />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition />
</Grid.RowDefinitions>
<Border BorderBrush="#505050" Grid.Column="2" Margin="0,1,1,1" Grid.RowSpan="2" BorderThickness="3,0,3,0" Background="#202020" CornerRadius="0" />
<Border Background="{Binding ColorBrush}" BorderBrush="Black" Grid.Column="1" BorderThickness="1" Grid.RowSpan="2" x:Name="ColorBorder" CornerRadius="2,0,0,2"/>
<Border Grid.RowSpan="2" Grid.ColumnSpan="2" Grid.Column="1" Background="Transparent" BorderThickness="1" BorderBrush="Black" CornerRadius="2"/>
<TextBlock Margin="7,5,5,5" Foreground="White" Grid.Row="0" Grid.Column="2" Text="{Binding HexColor}"/>
<TextBlock Margin="7,0,0,0" Foreground="White" Grid.Row="1" Grid.Column="2" Text="{Binding RgbColor}"/>
<Border Grid.Column="2"
Margin="0,1,1,1"
Background="#202020"
BorderBrush="#505050"
BorderThickness="3,0,3,0"
CornerRadius="0" />
<Border Grid.Column="1"
Background="{Binding ColorBrush}"
BorderBrush="Black"
BorderThickness="1"
CornerRadius="2,0,0,2" />
<Border Grid.Column="1"
Grid.ColumnSpan="2"
Background="Transparent"
BorderThickness="1"
BorderBrush="Black"
CornerRadius="2" />
<TextBlock Grid.Column="2"
VerticalAlignment="Center"
Margin="7,0,0,0"
Foreground="White"
Text="{Binding ColorText}" />
</Grid>
</Grid>
</UserControl>

View File

@@ -12,8 +12,6 @@ namespace ColorPicker.Views
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
}
=> InitializeComponent();
}
}

View File

@@ -1,24 +1,39 @@
<UserControl x:Class="ColorPicker.Views.ZoomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ColorPicker.Views"
mc:Ignorable="d"
xmlns:shaders="clr-namespace:ColorPicker.Shaders"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:behaviors="clr-namespace:ColorPicker.Behaviors"
d:DesignHeight="450" d:DesignWidth="800" Background="Transparent" Focusable="False">
<Border BorderBrush="#FF2B2B2B" ClipToBounds="True" BorderThickness="2" CornerRadius="2">
<Image Source="{Binding ZoomArea}" RenderOptions.BitmapScalingMode="NearestNeighbor" Stretch="Fill" Width="{Binding Width, Mode=TwoWay}" Height="{Binding Height, Mode=TwoWay}">
xmlns:shaders="clr-namespace:ColorPicker.Shaders"
Background="Transparent"
Focusable="False"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d" >
<Border BorderBrush="#FF2B2B2B"
BorderThickness="2"
ClipToBounds="True"
CornerRadius="2" >
<Image Source="{Binding ZoomArea}"
Height="{Binding Height, Mode=TwoWay}"
Width="{Binding Width, Mode=TwoWay}"
RenderOptions.BitmapScalingMode="NearestNeighbor"
Stretch="Fill" >
<e:Interaction.Behaviors>
<behaviors:ResizeBehavior Width="{Binding DesiredWidth}" Height="{Binding DesiredHeight}"/>
<behaviors:GridEffectBehavior Effect="{Binding ElementName=gridEffect}" ZoomFactor="{Binding ZoomFactor}"/>
<behaviors:ResizeBehavior Width="{Binding DesiredWidth}" Height="{Binding DesiredHeight}" />
<behaviors:GridEffectBehavior Effect="{Binding ElementName=gridEffect}" ZoomFactor="{Binding ZoomFactor}" />
</e:Interaction.Behaviors>
<Image.Effect>
<shaders:GridShaderEffect x:Name="gridEffect"/>
<shaders:GridShaderEffect x:Name="gridEffect" />
</Image.Effect>
</Image>
</Border>
</UserControl>

View File

@@ -12,8 +12,6 @@ namespace ColorPicker.Views
public partial class ZoomView : UserControl
{
public ZoomView()
{
InitializeComponent();
}
=> InitializeComponent();
}
}