Browse Source

Implement color display name in ColorSpectrum

pull/8050/head
robloo 5 years ago
parent
commit
25a34efd9a
  1. 2
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
  2. 60
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  3. 24
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs
  4. 116
      src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs
  5. 16
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml
  6. 16
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml

2
src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs

@ -41,7 +41,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public bool ShowAccentColors
{
get => (bool)this.GetValue(ShowAccentColorsProperty);
get => GetValue(ShowAccentColorsProperty);
set => SetValue(ShowAccentColorsProperty, value);
}
}

60
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

@ -20,7 +20,6 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// A two dimensional spectrum for color selection.
/// </summary>
[TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))]
[TemplatePart("PART_InputTarget", typeof(Canvas))]
[TemplatePart("PART_LayoutRoot", typeof(Panel))]
[TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))]
@ -60,7 +59,6 @@ namespace Avalonia.Controls.Primitives
private Ellipse? _spectrumOverlayEllipse;
private Canvas? _inputTarget;
private Panel? _selectionEllipsePanel;
private ToolTip? _colorNameToolTip;
// Put the spectrum images in a bitmap, which is then given to an ImageBrush.
private WriteableBitmap? _hueRedBitmap;
@ -117,7 +115,6 @@ namespace Avalonia.Controls.Primitives
UnregisterEvents(); // Failsafe
_colorNameToolTip = e.NameScope.Find<ToolTip>("PART_ColorNameToolTip");
_inputTarget = e.NameScope.Find<Canvas>("PART_InputTarget");
_layoutRoot = e.NameScope.Find<Panel>("PART_LayoutRoot");
_selectionEllipsePanel = e.NameScope.Find<Panel>("PART_SelectionEllipsePanel");
@ -152,10 +149,10 @@ namespace Avalonia.Controls.Primitives
});
}
if (ColorHelpers.ToDisplayNameExists &&
_colorNameToolTip != null)
if (_selectionEllipsePanel != null &&
ColorNameHelpers.ToDisplayNameExists)
{
_colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color);
ToolTip.SetTip(_selectionEllipsePanel, ColorNameHelpers.ToDisplayName(Color));
}
// If we haven't yet created our bitmaps, do so now.
@ -338,26 +335,45 @@ namespace Avalonia.Controls.Primitives
protected override void OnGotFocus(GotFocusEventArgs e)
{
// We only want to bother with the color name tool tip if we can provide color names.
if (_colorNameToolTip != null &&
ColorHelpers.ToDisplayNameExists)
if (_selectionEllipsePanel != null &&
ColorNameHelpers.ToDisplayNameExists)
{
ToolTip.SetIsOpen(_colorNameToolTip, true);
ToolTip.SetIsOpen(_selectionEllipsePanel, true);
}
UpdatePseudoClasses();
base.OnGotFocus(e);
}
/// <inheritdoc/>
protected override void OnLostFocus(RoutedEventArgs e)
{
// We only want to bother with the color name tool tip if we can provide color names.
if (_colorNameToolTip != null &&
ColorHelpers.ToDisplayNameExists)
if (_selectionEllipsePanel != null &&
ColorNameHelpers.ToDisplayNameExists)
{
ToolTip.SetIsOpen(_selectionEllipsePanel, false);
}
UpdatePseudoClasses();
base.OnLostFocus(e);
}
/// <inheritdoc/>
protected override void OnPointerLeave(PointerEventArgs e)
{
// We only want to bother with the color name tool tip if we can provide color names.
if (_selectionEllipsePanel != null &&
ColorNameHelpers.ToDisplayNameExists)
{
ToolTip.SetIsOpen(_colorNameToolTip, false);
ToolTip.SetIsOpen(_selectionEllipsePanel, false);
}
UpdatePseudoClasses();
base.OnPointerLeave(e);
}
/// <inheritdoc/>
@ -516,12 +532,10 @@ namespace Avalonia.Controls.Primitives
var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor);
ColorChanged?.Invoke(this, colorChangedEventArgs);
if (ColorHelpers.ToDisplayNameExists)
if (_selectionEllipsePanel != null &&
ColorNameHelpers.ToDisplayNameExists)
{
if (_colorNameToolTip != null)
{
_colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor);
}
ToolTip.SetTip(_selectionEllipsePanel, ColorNameHelpers.ToDisplayName(Color));
}
}
}
@ -811,15 +825,11 @@ namespace Avalonia.Controls.Primitives
Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2));
// We only want to bother with the color name tool tip if we can provide color names.
if (ColorHelpers.ToDisplayNameExists)
if (IsFocused &&
_selectionEllipsePanel != null &&
ColorNameHelpers.ToDisplayNameExists)
{
if (_colorNameToolTip != null)
{
// ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves,
// so toggling IsEnabled induces it to do that without incurring any visual glitches.
_colorNameToolTip.IsEnabled = false;
_colorNameToolTip.IsEnabled = true;
}
ToolTip.SetIsOpen(_selectionEllipsePanel, true);
}
UpdatePseudoClasses();

24
src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs

@ -17,18 +17,6 @@ namespace Avalonia.Controls.Primitives
{
internal static class ColorHelpers
{
public const int CheckerSize = 4;
public static bool ToDisplayNameExists
{
get => false;
}
public static string ToDisplayName(Color color)
{
return string.Empty;
}
/// <summary>
/// Generates a new bitmap of the specified size by changing a specific color component.
/// This will produce a gradient representing a sweep of all possible values of the color component.
@ -325,7 +313,7 @@ namespace Avalonia.Controls.Primitives
{
Hsv newHsv = originalHsv;
if (amount == IncrementAmount.Small || !ToDisplayNameExists)
if (amount == IncrementAmount.Small || !ColorNameHelpers.ToDisplayNameExists)
{
// In order to avoid working with small values that can incur rounding issues,
// we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1.
@ -416,7 +404,7 @@ namespace Avalonia.Controls.Primitives
// in the middle of that color's bounds.
Hsv newHsv = originalHsv;
string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor());
string originalColorName = ColorNameHelpers.ToDisplayName(originalHsv.ToRgb().ToColor());
string newColorName = originalColorName;
// Note: *newValue replaced with ref local variable for C#, must be initialized
@ -471,7 +459,7 @@ namespace Avalonia.Controls.Primitives
{
newValue = maxBound;
shouldFindMidPoint = false;
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
break;
}
}
@ -486,7 +474,7 @@ namespace Avalonia.Controls.Primitives
{
newValue = minBound;
shouldFindMidPoint = false;
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
break;
}
}
@ -501,7 +489,7 @@ namespace Avalonia.Controls.Primitives
break;
}
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
}
if (shouldFindMidPoint)
@ -574,7 +562,7 @@ namespace Avalonia.Controls.Primitives
}
}
currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor());
currentColorName = ColorNameHelpers.ToDisplayName(currentHsv.ToRgb().ToColor());
}
newValue = (startValue + currentValue + startEndOffset) / 2;

116
src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs

@ -0,0 +1,116 @@
using System;
using System.Globalization;
using System.Collections.Generic;
using Avalonia.Media;
using System.Text;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Contains helpers useful when working with color names.
/// </summary>
public static class ColorNameHelpers
{
private static readonly Dictionary<Color, string> cachedDisplayNames = new Dictionary<Color, string>();
private static readonly object cacheMutex = new object();
/// <summary>
/// Determines if color display names are supported based on the current thread culture.
/// </summary>
/// <remarks>
/// Only English names are currently supported following known color names.
/// In the future known color names could be localized.
/// </remarks>
public static bool ToDisplayNameExists
{
get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines an approximate display name for the given color.
/// </summary>
/// <param name="color">The color to get the display name for.</param>
/// <returns>The approximate color display name.</returns>
public static string ToDisplayName(Color color)
{
// Without rounding, there are 16,777,216 possible RGB colors (without alpha).
// This is too many to cache and search through for performance reasons.
// It is also needlessly large as there are only ~140 known/named colors.
// Therefore, rounding of the input color's component values is done to
// reduce the color space into something more useful.
double rounding = 5;
var roundedColor = new Color(
0xFF,
Convert.ToByte(Math.Round(color.R / rounding) * rounding),
Convert.ToByte(Math.Round(color.G / rounding) * rounding),
Convert.ToByte(Math.Round(color.B / rounding) * rounding));
// Attempt to use a previously cached display name
lock (cacheMutex)
{
if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName))
{
return displayName;
}
}
// Find the closest known color by measuring 3D Euclidean distance (ignore alpha)
var closestKnownColor = KnownColor.None;
var closestKnownColorDistance = double.PositiveInfinity;
var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
for (int i = 1; i < knownColors.Length; i++) // Skip 'None'
{
// Transparent is skipped since alpha is ignored making it equivalent to White
if (knownColors[i] != KnownColor.Transparent)
{
Color knownColor = KnownColors.ToColor(knownColors[i]);
double distance = Math.Sqrt(
Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) +
Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) +
Math.Pow((double)(roundedColor.B - knownColor.B), 2.0));
if (distance < closestKnownColorDistance)
{
closestKnownColor = knownColors[i];
closestKnownColorDistance = distance;
}
}
}
// Return the closest known color as the display name
// Cache results for next time as well
if (closestKnownColor != KnownColor.None)
{
StringBuilder sb = new StringBuilder();
string name = closestKnownColor.ToString();
// Add spaces converting PascalCase to human-readable names
for (int i = 0; i < name.Length; i++)
{
if (i != 0 &&
char.IsUpper(name[i]))
{
sb.Append(' ');
}
sb.Append(name[i]);
}
string displayName = sb.ToString();
lock (cacheMutex)
{
cachedDisplayNames.Add(roundedColor, displayName);
}
return displayName;
}
else
{
return string.Empty;
}
}
}
}

16
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml

@ -48,7 +48,10 @@
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Panel x:Name="PART_SelectionEllipsePanel">
<!-- Note: ToolTip.VerticalOffset is for touch devices to keep the tip above fingers -->
<Panel x:Name="PART_SelectionEllipsePanel"
ToolTip.VerticalOffset="-10"
ToolTip.Placement="Top">
<Ellipse x:Name="FocusEllipse"
Margin="-2"
StrokeThickness="2"
@ -59,13 +62,10 @@
StrokeThickness="2"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ToolTip.VerticalOffset="-20"
ToolTip.Placement="Top">
<ToolTip.Tip>
<ToolTip x:Name="PART_ColorNameToolTip" />
</ToolTip.Tip>
</Ellipse>
VerticalAlignment="Stretch" />
<ToolTip.Tip>
<!-- Set in code-behind -->
</ToolTip.Tip>
</Panel>
</Canvas>
<Rectangle x:Name="BorderRectangle"

16
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml

@ -48,7 +48,10 @@
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Panel x:Name="PART_SelectionEllipsePanel">
<!-- Note: ToolTip.VerticalOffset is for touch devices to keep the tip above fingers -->
<Panel x:Name="PART_SelectionEllipsePanel"
ToolTip.VerticalOffset="-10"
ToolTip.Placement="Top">
<Ellipse x:Name="FocusEllipse"
Margin="-2"
StrokeThickness="2"
@ -59,13 +62,10 @@
StrokeThickness="2"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ToolTip.VerticalOffset="-20"
ToolTip.Placement="Top">
<ToolTip.Tip>
<ToolTip x:Name="PART_ColorNameToolTip" />
</ToolTip.Tip>
</Ellipse>
VerticalAlignment="Stretch" />
<ToolTip.Tip>
<!-- Set in code-behind -->
</ToolTip.Tip>
</Panel>
</Canvas>
<Rectangle x:Name="BorderRectangle"

Loading…
Cancel
Save