diff --git a/Directory.Build.props b/Directory.Build.props index d0e8f102..51c3f6dd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,6 +10,7 @@ $(RepositoryDirectory)components\Converters\src\CommunityToolkit.WinUI.Converters.csproj $(RepositoryDirectory)components\Extensions\src\CommunityToolkit.WinUI.Extensions.csproj $(RepositoryDirectory)components\Triggers\src\CommunityToolkit.WinUI.Triggers.csproj + $(RepositoryDirectory)components\Segmented\src\CommunityToolkit.WinUI.Controls.Segmented.csproj $(RepositoryDirectory)components\SettingsControls\src\CommunityToolkit.WinUI.Controls.SettingsControls.csproj diff --git a/components/ColorPicker/OpenSolution.bat b/components/ColorPicker/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/ColorPicker/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/ColorPicker/samples/Assets/ColorPicker.png b/components/ColorPicker/samples/Assets/ColorPicker.png new file mode 100644 index 00000000..3fbba28b Binary files /dev/null and b/components/ColorPicker/samples/Assets/ColorPicker.png differ diff --git a/components/ColorPicker/samples/ColorPicker.Samples.csproj b/components/ColorPicker/samples/ColorPicker.Samples.csproj new file mode 100644 index 00000000..741a87ef --- /dev/null +++ b/components/ColorPicker/samples/ColorPicker.Samples.csproj @@ -0,0 +1,18 @@ + + + + ColorPicker + + + + + + + + PreserveNewest + + + + + + diff --git a/components/ColorPicker/samples/ColorPicker.md b/components/ColorPicker/samples/ColorPicker.md new file mode 100644 index 00000000..94696882 --- /dev/null +++ b/components/ColorPicker/samples/ColorPicker.md @@ -0,0 +1,22 @@ +--- +title: ColorPicker +author: michael-hawker +description: An extended color picker control that lets a user pick a color using a color spectrum, sliders, or text input. +keywords: ColorPicker, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Input +discussion-id: 0 +issue-id: 0 +icon: Assets/ColorPicker.png +--- + +The [ColorPicker](/dotnet/api/microsoft.toolkit.uwp.ui.controls.colorpicker) control lets a user pick a color using a color spectrum, palette, sliders, or text input. + +> [!Sample ColorPickerSample] + +## ColorPickerButton +The `ColorPickerButton` variant represents a `DropDownButton` variant which provides a preview of the selected color and allows a user to expand the drop-down to select a new color. + +> [!Sample ColorPickerButtonSample] diff --git a/components/ColorPicker/samples/ColorPickerButtonSample.xaml b/components/ColorPicker/samples/ColorPickerButtonSample.xaml new file mode 100644 index 00000000..c473c4b2 --- /dev/null +++ b/components/ColorPicker/samples/ColorPickerButtonSample.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/components/ColorPicker/samples/ColorPickerButtonSample.xaml.cs b/components/ColorPicker/samples/ColorPickerButtonSample.xaml.cs new file mode 100644 index 00000000..f2e1bb19 --- /dev/null +++ b/components/ColorPicker/samples/ColorPickerButtonSample.xaml.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using ColorSpectrumShape = Microsoft.UI.Xaml.Controls.ColorSpectrumShape; + +namespace ColorPickerExperiment.Samples; + +[ToolkitSample(id: nameof(ColorPickerButtonSample), "ColorPickerButton", description: $"A sample for showing how to create and use a {nameof(CommunityToolkit.WinUI.Controls.ColorPickerButton)} control.")] +public sealed partial class ColorPickerButtonSample : Page +{ + public ColorPickerButtonSample() + { + this.InitializeComponent(); + } +} diff --git a/components/ColorPicker/samples/ColorPickerSample.xaml b/components/ColorPicker/samples/ColorPickerSample.xaml new file mode 100644 index 00000000..9ad7a693 --- /dev/null +++ b/components/ColorPicker/samples/ColorPickerSample.xaml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/components/ColorPicker/samples/ColorPickerSample.xaml.cs b/components/ColorPicker/samples/ColorPickerSample.xaml.cs new file mode 100644 index 00000000..f632aa9c --- /dev/null +++ b/components/ColorPicker/samples/ColorPickerSample.xaml.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using ColorSpectrumShape = Microsoft.UI.Xaml.Controls.ColorSpectrumShape; + +namespace ColorPickerExperiment.Samples; + +[ToolkitSampleBoolOption("AccentColors", true, Title = "ShowAccentColors")] +[ToolkitSampleBoolOption("AlphaEnabled", true, Title = "IsAlphaEnabled")] +[ToolkitSampleBoolOption("AlphaSlider", true, Title = "IsAlphaSliderVisible")] +[ToolkitSampleBoolOption("ColorSlider", true, Title = "IsColorSliderVisible")] +[ToolkitSampleBoolOption("ColorChannel", true, Title = "IsColorChannelTextInputVisible")] +[ToolkitSampleBoolOption("SpectrumVisible", true, Title = "IsColorSpectrumVisible")] +[ToolkitSampleBoolOption("ColorPalette", true, Title = "IsColorPaletteVisible")] + +[ToolkitSampleMultiChoiceOption("SpectrumShape", "Box", "Ring", Title = "ColorSpectrumShape")] + +[ToolkitSample(id: nameof(ColorPickerSample), "ColorPicker", description: $"A sample for showing how to create and use a {nameof(CommunityToolkit.WinUI.Controls.ColorPicker)} control.")] +public sealed partial class ColorPickerSample : Page +{ + public ColorPickerSample() + { + this.InitializeComponent(); + } + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static ColorSpectrumShape ConvertStringToColorSpectrumShape(string shape) => shape switch + { + "Ring" => ColorSpectrumShape.Ring, + "Box" => ColorSpectrumShape.Box, + _ => throw new System.NotImplementedException(), + }; +} diff --git a/components/ColorPicker/samples/Dependencies.props b/components/ColorPicker/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/ColorPicker/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/ColorPicker/src/ColorChannel.cs b/components/ColorPicker/src/ColorChannel.cs new file mode 100644 index 00000000..25a7e6c5 --- /dev/null +++ b/components/ColorPicker/src/ColorChannel.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Defines a specific channel within a color representation. +/// +[EditorBrowsable(EditorBrowsableState.Advanced)] +public enum ColorChannel +{ + /// + /// Represents the alpha channel. + /// + Alpha, + + /// + /// Represents the first color channel which is Red when RGB or Hue when HSV. + /// + Channel1, + + /// + /// Represents the second color channel which is Green when RGB or Saturation when HSV. + /// + Channel2, + + /// + /// Represents the third color channel which is Blue when RGB or Value when HSV. + /// + Channel3 +} diff --git a/components/ColorPicker/src/ColorPicker.Properties.cs b/components/ColorPicker/src/ColorPicker.Properties.cs new file mode 100644 index 00000000..509aef54 --- /dev/null +++ b/components/ColorPicker/src/ColorPicker.Properties.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Contains all properties for the . +/// +public partial class ColorPicker +{ + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CustomPaletteColorsProperty = + DependencyProperty.Register( + nameof(CustomPaletteColors), + typeof(ObservableCollection), + typeof(ColorPicker), + new PropertyMetadata( + null, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets the list of custom palette colors. + /// + public ObservableCollection CustomPaletteColors + { + get => (ObservableCollection)this.GetValue(CustomPaletteColorsProperty); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CustomPaletteColumnCountProperty = + DependencyProperty.Register( + nameof(CustomPaletteColumnCount), + typeof(int), + typeof(ColorPicker), + new PropertyMetadata( + 4, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the number of colors in each row (section) of the custom color palette. + /// Within a standard palette, rows are shades and columns are unique colors. + /// + public int CustomPaletteColumnCount + { + get => (int)this.GetValue(CustomPaletteColumnCountProperty); + set + { + if (object.Equals(value, this.GetValue(CustomPaletteColumnCountProperty)) == false) + { + this.SetValue(CustomPaletteColumnCountProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CustomPaletteProperty = + DependencyProperty.Register( + nameof(CustomPalette), + typeof(IColorPalette), + typeof(ColorPicker), + new PropertyMetadata( + null, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the custom color palette. + /// This will automatically set and + /// overwriting any existing values. + /// + public IColorPalette CustomPalette + { + get => (IColorPalette)this.GetValue(CustomPaletteProperty); + set + { + if (object.Equals(value, this.GetValue(CustomPaletteProperty)) == false) + { + this.SetValue(CustomPaletteProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsColorPaletteVisibleProperty = + DependencyProperty.Register( + nameof(IsColorPaletteVisible), + typeof(bool), + typeof(ColorPicker), + new PropertyMetadata( + true, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets a value indicating whether the color palette is visible. + /// + public bool IsColorPaletteVisible + { + get => (bool)this.GetValue(IsColorPaletteVisibleProperty); + set + { + if (object.Equals(value, this.GetValue(IsColorPaletteVisibleProperty)) == false) + { + this.SetValue(IsColorPaletteVisibleProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ShowAccentColorsProperty = + DependencyProperty.Register( + nameof(ShowAccentColors), + typeof(bool), + typeof(ColorPicker), + new PropertyMetadata( + true, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets a value indicating whether accent colors are shown along + /// with the preview color. + /// + public bool ShowAccentColors + { + get => (bool)this.GetValue(ShowAccentColorsProperty); + set + { + if (object.Equals(value, this.GetValue(ShowAccentColorsProperty)) == false) + { + this.SetValue(ShowAccentColorsProperty, value); + } + } + } +} diff --git a/components/ColorPicker/src/ColorPicker.cs b/components/ColorPicker/src/ColorPicker.cs new file mode 100644 index 00000000..eb5e0690 --- /dev/null +++ b/components/ColorPicker/src/ColorPicker.cs @@ -0,0 +1,1451 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Controls.Primitives; +using CommunityToolkit.WinUI.Helpers; +using System.Collections.Specialized; +using Windows.UI; +using ColorSpectrum = Microsoft.UI.Xaml.Controls.Primitives.ColorSpectrum; +using ColorPickerSlider = CommunityToolkit.WinUI.Controls.Primitives.ColorPickerSlider; +#if WINAPPSDK || HAS_UNO && WINUI +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Dispatching; +using Colors = Microsoft.UI.Colors; +#elif WINDOWS_UWP || HAS_UNO && WINUI2 +using Windows.System; +using Windows.UI.Xaml.Input; +using Microsoft.UI.Xaml.Controls; +using Colors = Windows.UI.Colors; +#endif +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Presents a color spectrum, a palette of colors, and color channel sliders for user selection of a color. +/// +[TemplatePart(Name = nameof(ColorPicker.AlphaChannelNumberBox), Type = typeof(NumberBox))] +[TemplatePart(Name = nameof(ColorPicker.AlphaChannelSlider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.Channel1NumberBox), Type = typeof(NumberBox))] +[TemplatePart(Name = nameof(ColorPicker.Channel1Slider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.Channel2NumberBox), Type = typeof(NumberBox))] +[TemplatePart(Name = nameof(ColorPicker.Channel2Slider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.Channel3NumberBox), Type = typeof(NumberBox))] +[TemplatePart(Name = nameof(ColorPicker.Channel3Slider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground1Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground2Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground3Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground4Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground5Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground6Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground7Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground8Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground9Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground10Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.ColorPanelSelector), Type = typeof(Segmented))] +[TemplatePart(Name = nameof(ColorPicker.ColorSpectrumControl), Type = typeof(ColorSpectrum))] +[TemplatePart(Name = nameof(ColorPicker.ColorSpectrumAlphaSlider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.ColorSpectrumThirdDimensionSlider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.HexInputTextBox), Type = typeof(TextBox))] +[TemplatePart(Name = nameof(ColorPicker.ColorModeComboBox), Type = typeof(ComboBox))] + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1501:Statement should not be on a single line", Justification = "Inline brackets are used to improve code readability with repeated null checks.")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Whitespace is used to align code in columns for readability.")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Only template parts start with a capital letter. This differentiates them from other fields.")] +public partial class ColorPicker : Microsoft.UI.Xaml.Controls.ColorPicker +{ + internal Color CheckerBackgroundColor { get; set; } = Color.FromArgb(0x19, 0x80, 0x80, 0x80); // Overridden later + + /// + /// The period that scheduled color updates will be applied. + /// This is only used when updating colors using the ScheduleColorUpdate() method. + /// Color changes made directly to the Color property will apply instantly. + /// + private const int ColorUpdateInterval = 30; // Milliseconds + + private long tokenColor; + + private bool callbacksConnected = false; + private bool eventsConnected = false; + private bool isInitialized = false; + + // Color information for updates + private HsvColor? savedHsvColor = null; + private Color? savedHsvColorRgbEquivalent = null; + private Color? updatedRgbColor = null; + private DispatcherQueueTimer? dispatcherQueueTimer = null; + + private Segmented ColorPanelSelector; + private ColorSpectrum ColorSpectrumControl; + private ColorPickerSlider ColorSpectrumAlphaSlider; + private ColorPickerSlider ColorSpectrumThirdDimensionSlider; + private TextBox HexInputTextBox; + private ComboBox ColorModeComboBox; + + private NumberBox Channel1NumberBox; + private NumberBox Channel2NumberBox; + private NumberBox Channel3NumberBox; + private NumberBox AlphaChannelNumberBox; + private ColorPickerSlider Channel1Slider; + private ColorPickerSlider Channel2Slider; + private ColorPickerSlider Channel3Slider; + private ColorPickerSlider AlphaChannelSlider; + + private ColorPreviewer ColorPreviewer; + + // Up to 10 checkered backgrounds may be used by name anywhere in the template + private Border CheckeredBackground1Border; + private Border CheckeredBackground2Border; + private Border CheckeredBackground3Border; + private Border CheckeredBackground4Border; + private Border CheckeredBackground5Border; + private Border CheckeredBackground6Border; + private Border CheckeredBackground7Border; + private Border CheckeredBackground8Border; + private Border CheckeredBackground9Border; + private Border CheckeredBackground10Border; + + /*************************************************************************************** + * + * Constructor/Destructor + * + ***************************************************************************************/ + + /// + /// Initializes a new instance of the class. + /// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public ColorPicker() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + this.DefaultStyleKey = typeof(ColorPicker); + + // Workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/3502 + this.DefaultStyleResourceUri = new Uri("ms-appx:///CommunityToolkit.WinUI.Controls.ColorPicker/Themes/Generic.xaml"); + + // Setup collections + this.SetValue(CustomPaletteColorsProperty, new ObservableCollection()); + this.CustomPaletteColors.CollectionChanged += CustomPaletteColors_CollectionChanged; + + this.Loaded += ColorPickerButton_Loaded; + + // Checkered background color is found only one time for performance + // This may need to change in the future if theme changes should be supported + this.CheckerBackgroundColor = (Color)Application.Current.Resources["SystemListLowColor"]; + + this.ConnectCallbacks(true); + this.SetDefaultPalette(); + this.StartDispatcherQueueTimer(); + this.RegisterPropertyChangedCallback(IsColorChannelTextInputVisibleProperty, OnPanelVisibilityChanged); + this.RegisterPropertyChangedCallback(IsColorSpectrumVisibleProperty, OnPanelVisibilityChanged); + } + + /// + /// Finalizes an instance of the class. + /// + ~ColorPicker() + { + this.StopDispatcherQueueTimer(); + this.CustomPaletteColors.CollectionChanged -= CustomPaletteColors_CollectionChanged; + } + + /*************************************************************************************** + * + * Methods + * + ***************************************************************************************/ + + /// + /// Gets whether or not the color is considered empty (all fields zero). + /// In the future Color.IsEmpty will hopefully be added to UWP. + /// + /// The Windows.UI.Color to calculate with. + /// Whether the color is considered empty. + private static bool IsColorEmpty(Color color) + { + return color.A == 0x00 && + color.R == 0x00 && + color.G == 0x00 && + color.B == 0x00; + } + + /// + /// Overrides when a template is applied in order to get the required controls. + /// + protected override void OnApplyTemplate() + { + // We need to disconnect old events first + this.ConnectEvents(false); + + this.ColorPanelSelector = (Segmented)GetTemplateChild(nameof(ColorPanelSelector)); + + this.ColorSpectrumControl = (ColorSpectrum)GetTemplateChild(nameof(ColorSpectrumControl)); + this.ColorSpectrumAlphaSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(ColorSpectrumAlphaSlider)); + this.ColorSpectrumThirdDimensionSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(ColorSpectrumThirdDimensionSlider)); + + this.HexInputTextBox = (TextBox)this.GetTemplateChild(nameof(HexInputTextBox)); + this.ColorModeComboBox = (ComboBox)this.GetTemplateChild(nameof(ColorModeComboBox)); + + this.Channel1NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel1NumberBox)); + this.Channel2NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel2NumberBox)); + this.Channel3NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel3NumberBox)); + this.AlphaChannelNumberBox = (NumberBox)this.GetTemplateChild(nameof(AlphaChannelNumberBox)); + + this.Channel1Slider = (ColorPickerSlider)this.GetTemplateChild(nameof(Channel1Slider)); + this.Channel2Slider = (ColorPickerSlider)this.GetTemplateChild(nameof(Channel2Slider)); + this.Channel3Slider = (ColorPickerSlider)this.GetTemplateChild(nameof(Channel3Slider)); + this.AlphaChannelSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(AlphaChannelSlider)); + + this.ColorPreviewer = (ColorPreviewer)this.GetTemplateChild(nameof(ColorPreviewer)); + + this.CheckeredBackground1Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground1Border)); + this.CheckeredBackground2Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground2Border)); + this.CheckeredBackground3Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground3Border)); + this.CheckeredBackground4Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground4Border)); + this.CheckeredBackground5Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground5Border)); + this.CheckeredBackground6Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground6Border)); + this.CheckeredBackground7Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground7Border)); + this.CheckeredBackground8Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground8Border)); + this.CheckeredBackground9Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground9Border)); + this.CheckeredBackground10Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground10Border)); + + // Must connect after controls are resolved + this.ConnectEvents(true); + + base.OnApplyTemplate(); + this.UpdateVisualState(false); + this.ValidateSelectedPanel(); + this.isInitialized = true; + + this.SetActiveColorRepresentation(ColorRepresentation.Rgba); + this.UpdateColorControlValues(); // TODO: This will also connect events after, can we optimize vs. doing it twice with the ConnectEvents above? + } + + /// + /// Connects or disconnects all dependency property callbacks. + /// + private void ConnectCallbacks(bool connected) + { + if (connected == true && + this.callbacksConnected == false) + { + // Add callbacks for dependency properties + this.tokenColor = this.RegisterPropertyChangedCallback(ColorProperty, OnColorChanged); + + this.callbacksConnected = true; + } + else if (connected == false && + this.callbacksConnected == true) + { + // Remove callbacks for dependency properties + this.UnregisterPropertyChangedCallback(ColorProperty, this.tokenColor); + + this.callbacksConnected = false; + } + + return; + } + + /// + /// Connects or disconnects all control event handlers. + /// + /// True to connect event handlers, otherwise false. + private void ConnectEvents(bool connected) + { + if (connected == true && + this.eventsConnected == false) + { + // Add all events + if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.ColorChanged += ColorSpectrum_ColorChanged; } + if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.GotFocus += ColorSpectrum_GotFocus; } + if (this.HexInputTextBox != null) { this.HexInputTextBox.KeyDown += HexInputTextBox_KeyDown; } + if (this.HexInputTextBox != null) { this.HexInputTextBox.LostFocus += HexInputTextBox_LostFocus; } + if (this.ColorModeComboBox != null) { this.ColorModeComboBox.SelectionChanged += ColorModeComboBox_SelectionChanged; } + + if (this.Channel1NumberBox != null) { this.Channel1NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } + if (this.Channel2NumberBox != null) { this.Channel2NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } + if (this.Channel3NumberBox != null) { this.Channel3NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } + if (this.AlphaChannelNumberBox != null) { this.AlphaChannelNumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } + + if (this.Channel1Slider != null) { this.Channel1Slider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.Channel2Slider != null) { this.Channel2Slider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.Channel3Slider != null) { this.Channel3Slider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.ValueChanged += ChannelSlider_ValueChanged; } + + if (this.Channel1Slider != null) { this.Channel1Slider.Loaded += ChannelSlider_Loaded; } + if (this.Channel2Slider != null) { this.Channel2Slider.Loaded += ChannelSlider_Loaded; } + if (this.Channel3Slider != null) { this.Channel3Slider.Loaded += ChannelSlider_Loaded; } + if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.Loaded += ChannelSlider_Loaded; } + if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.Loaded += ChannelSlider_Loaded; } + if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.Loaded += ChannelSlider_Loaded; } + + if (this.ColorPreviewer != null) { this.ColorPreviewer.ColorChangeRequested += ColorPreviewer_ColorChangeRequested; } + + if (this.CheckeredBackground1Border != null) { this.CheckeredBackground1Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground2Border != null) { this.CheckeredBackground2Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground3Border != null) { this.CheckeredBackground3Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground4Border != null) { this.CheckeredBackground4Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground5Border != null) { this.CheckeredBackground5Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground6Border != null) { this.CheckeredBackground6Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground7Border != null) { this.CheckeredBackground7Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground8Border != null) { this.CheckeredBackground8Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground9Border != null) { this.CheckeredBackground9Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground10Border != null) { this.CheckeredBackground10Border.Loaded += CheckeredBackgroundBorder_Loaded; } + + this.eventsConnected = true; + } + else if (connected == false && + this.eventsConnected == true) + { + // Remove all events + if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.ColorChanged -= ColorSpectrum_ColorChanged; } + if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.GotFocus -= ColorSpectrum_GotFocus; } + if (this.HexInputTextBox != null) { this.HexInputTextBox.KeyDown -= HexInputTextBox_KeyDown; } + if (this.HexInputTextBox != null) { this.HexInputTextBox.LostFocus -= HexInputTextBox_LostFocus; } + if (this.ColorModeComboBox != null) { this.ColorModeComboBox.SelectionChanged -= ColorModeComboBox_SelectionChanged; } + + if (this.Channel1NumberBox != null) { this.Channel1NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } + if (this.Channel2NumberBox != null) { this.Channel2NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } + if (this.Channel3NumberBox != null) { this.Channel3NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } + if (this.AlphaChannelNumberBox != null) { this.AlphaChannelNumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } + + if (this.Channel1Slider != null) { this.Channel1Slider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.Channel2Slider != null) { this.Channel2Slider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.Channel3Slider != null) { this.Channel3Slider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.ValueChanged -= ChannelSlider_ValueChanged; } + + if (this.Channel1Slider != null) { this.Channel1Slider.Loaded -= ChannelSlider_Loaded; } + if (this.Channel2Slider != null) { this.Channel2Slider.Loaded -= ChannelSlider_Loaded; } + if (this.Channel3Slider != null) { this.Channel3Slider.Loaded -= ChannelSlider_Loaded; } + if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.Loaded -= ChannelSlider_Loaded; } + if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.Loaded -= ChannelSlider_Loaded; } + if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.Loaded -= ChannelSlider_Loaded; } + + if (this.ColorPreviewer != null) { this.ColorPreviewer.ColorChangeRequested -= ColorPreviewer_ColorChangeRequested; } + + if (this.CheckeredBackground1Border != null) { this.CheckeredBackground1Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground2Border != null) { this.CheckeredBackground2Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground3Border != null) { this.CheckeredBackground3Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground4Border != null) { this.CheckeredBackground4Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground5Border != null) { this.CheckeredBackground5Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground6Border != null) { this.CheckeredBackground6Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground7Border != null) { this.CheckeredBackground7Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground8Border != null) { this.CheckeredBackground8Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground9Border != null) { this.CheckeredBackground9Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground10Border != null) { this.CheckeredBackground10Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + + this.eventsConnected = false; + } + + return; + } + + + /// + /// Updates all visual states based on current control properties. + /// + /// Whether transitions should occur when changing states. + private void UpdateVisualState(bool useTransitions = true) + { + VisualStateManager.GoToState(this, this.IsEnabled ? "Normal" : "Disabled", useTransitions); + VisualStateManager.GoToState(this, this.GetActiveColorRepresentation() == ColorRepresentation.Hsva ? "HsvSelected" : "RgbSelected", useTransitions); + VisualStateManager.GoToState(this, this.IsColorPaletteVisible ? "ColorPaletteVisible" : "ColorPaletteCollapsed", useTransitions); + + // Check if only a single vie is selected and hide the Segmented control + VisualStateManager.GoToState(this, (Truth(IsColorPaletteVisible, IsColorSpectrumVisible, IsColorChannelTextInputVisible) <= 1) ? "ColorPanelSelectorCollapsed" : "ColorPanelSelectorVisible", useTransitions); + } + + public static int Truth(params bool[] booleans) + { + return booleans.Count(b => b); + } + + /// + /// Gets the active representation of the color: HSV or RGB. + /// + private ColorRepresentation GetActiveColorRepresentation() + { + // If the HSV representation control is missing for whatever reason, + // the default will be RGB + + if (this.ColorModeComboBox != null && + this.ColorModeComboBox.SelectedIndex == 1) + { + return ColorRepresentation.Hsva; + } + + return ColorRepresentation.Rgba; + } + + /// + /// Sets the active color representation in the UI controls. + /// + /// The color representation to set. + /// Setting to null (the default) will attempt to keep the current state. + private void SetActiveColorRepresentation(ColorRepresentation? colorRepresentation = null) + { + bool eventsDisconnectedByMethod = false; + + if (colorRepresentation == null) + { + // Use the control's current value + colorRepresentation = this.GetActiveColorRepresentation(); + } + + // Disable events during the update + if (this.eventsConnected) + { + this.ConnectEvents(false); + eventsDisconnectedByMethod = true; + } + + // Sync the UI controls and visual state + // The default is always RGBA + if (colorRepresentation == ColorRepresentation.Hsva) + { + this.ColorModeComboBox.SelectedIndex = 1; + } + else + { + this.ColorModeComboBox.SelectedIndex = 0; + } + + this.UpdateVisualState(false); + + if (eventsDisconnectedByMethod) + { + this.ConnectEvents(true); + } + + return; + } + + /// + /// Gets the active third dimension in the color spectrum: Hue, Saturation or Value. + /// + private ColorChannel GetActiveColorSpectrumThirdDimension() + { + switch (this.ColorSpectrumComponents) + { + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.SaturationValue: + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.ValueSaturation: + { + // Hue + return ColorChannel.Channel1; + } + + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.HueValue: + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.ValueHue: + { + // Saturation + return ColorChannel.Channel2; + } + + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.HueSaturation: + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.SaturationHue: + { + // Value + return ColorChannel.Channel3; + } + } + + return ColorChannel.Alpha; // Error, should never get here + } + + /// + /// Declares a new color to set to the control. + /// Application of this color will be scheduled to avoid overly rapid updates. + /// + /// The new color to set to the control. + private void ScheduleColorUpdate(Color newColor) + { + // Coerce the value as needed + if (this.IsAlphaEnabled == false) + { + newColor = new Color() + { + R = newColor.R, + G = newColor.G, + B = newColor.B, + A = 255 + }; + } + + this.updatedRgbColor = newColor; + + return; + } + + /// + /// Updates the color values in all editing controls to match the current color. + /// + private void UpdateColorControlValues() + { + bool eventsDisconnectedByMethod = false; + Color rgbColor = this.Color; + HsvColor hsvColor; + + if (this.isInitialized) + { + // Disable events during the update + if (this.eventsConnected) + { + this.ConnectEvents(false); + eventsDisconnectedByMethod = true; + } + + if (this.HexInputTextBox != null) + { + if (this.IsAlphaEnabled) + { + // Remove only the "#" sign + this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty); + } + else + { + // Remove the "#" sign and alpha hex + this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty).Substring(2); + } + } + + // Regardless of the active color representation, the spectrum is always HSV + // Therefore, always calculate HSV color here + // Warning: Always maintain/use HSV information in the saved HSV color + // This avoids loss of precision and drift caused by continuously converting to/from RGB + if (this.savedHsvColor == null) + { + hsvColor = rgbColor.ToHsv(); + + // Round the channels, be sure rounding matches with the scaling next + // Rounding of SVA requires at MINIMUM 2 decimal places + int decimals = 0; + hsvColor = new HsvColor() + { + H = Math.Round(hsvColor.H, decimals), + S = Math.Round(hsvColor.S, 2 + decimals), + V = Math.Round(hsvColor.V, 2 + decimals), + A = Math.Round(hsvColor.A, 2 + decimals) + }; + + // Must update HSV color + this.savedHsvColor = hsvColor; + this.savedHsvColorRgbEquivalent = rgbColor; + } + else + { + hsvColor = this.savedHsvColor.Value; + } + + // Update the color spectrum + // Remember the spectrum is always HSV and must be updated as such to avoid + // conversion errors + if (this.ColorSpectrumControl != null) + { + this.ColorSpectrumControl.HsvColor = new System.Numerics.Vector4() + { + X = Convert.ToSingle(hsvColor.H), + Y = Convert.ToSingle(hsvColor.S), + Z = Convert.ToSingle(hsvColor.V), + W = Convert.ToSingle(hsvColor.A) + }; + } + + // Update the color spectrum third dimension channel + if (this.ColorSpectrumThirdDimensionSlider != null) + { + // Convert the channels into a usable range for the user + double hue = hsvColor.H; + double staturation = hsvColor.S * 100; + double value = hsvColor.V * 100; + + switch (this.GetActiveColorSpectrumThirdDimension()) + { + case ColorChannel.Channel1: + { + // Hue + this.ColorSpectrumThirdDimensionSlider.Minimum = 0; + this.ColorSpectrumThirdDimensionSlider.Maximum = 360; + this.ColorSpectrumThirdDimensionSlider.Value = hue; + break; + } + + case ColorChannel.Channel2: + { + // Saturation + this.ColorSpectrumThirdDimensionSlider.Minimum = 0; + this.ColorSpectrumThirdDimensionSlider.Maximum = 100; + this.ColorSpectrumThirdDimensionSlider.Value = staturation; + break; + } + + case ColorChannel.Channel3: + { + // Value + this.ColorSpectrumThirdDimensionSlider.Minimum = 0; + this.ColorSpectrumThirdDimensionSlider.Maximum = 100; + this.ColorSpectrumThirdDimensionSlider.Value = value; + break; + } + } + } + + // Update the preview color + if (this.ColorPreviewer != null) + { + this.ColorPreviewer.HsvColor = hsvColor; + } + + // Update all other color channels + if (this.GetActiveColorRepresentation() == ColorRepresentation.Hsva) + { + // Convert the channels into a usable range for the user + double hue = hsvColor.H; + double staturation = hsvColor.S * 100; + double value = hsvColor.V * 100; + double alpha = hsvColor.A * 100; + + // Hue + if (this.Channel1NumberBox != null) + { + this.Channel1NumberBox.Minimum = 0; + this.Channel1NumberBox.Maximum = 360; + this.Channel1NumberBox.Value = hue; + } + + if (this.Channel1Slider != null) + { + this.Channel1Slider.Minimum = 0; + this.Channel1Slider.Maximum = 360; + this.Channel1Slider.Value = hue; + } + + // Saturation + if (this.Channel2NumberBox != null) + { + this.Channel2NumberBox.Minimum = 0; + this.Channel2NumberBox.Maximum = 100; + this.Channel2NumberBox.Value = staturation; + } + + if (this.Channel2Slider != null) + { + this.Channel2Slider.Minimum = 0; + this.Channel2Slider.Maximum = 100; + this.Channel2Slider.Value = staturation; + } + + // Value + if (this.Channel3NumberBox != null) + { + this.Channel3NumberBox.Minimum = 0; + this.Channel3NumberBox.Maximum = 100; + this.Channel3NumberBox.Value = value; + } + + if (this.Channel3Slider != null) + { + this.Channel3Slider.Minimum = 0; + this.Channel3Slider.Maximum = 100; + this.Channel3Slider.Value = value; + } + + // Alpha + if (this.AlphaChannelNumberBox != null) + { + this.AlphaChannelNumberBox.Minimum = 0; + this.AlphaChannelNumberBox.Maximum = 100; + this.AlphaChannelNumberBox.Value = alpha; + } + + if (this.AlphaChannelSlider != null) + { + this.AlphaChannelSlider.Minimum = 0; + this.AlphaChannelSlider.Maximum = 100; + this.AlphaChannelSlider.Value = alpha; + } + + // Color spectrum alpha + if (this.ColorSpectrumAlphaSlider != null) + { + this.ColorSpectrumAlphaSlider.Minimum = 0; + this.ColorSpectrumAlphaSlider.Maximum = 100; + this.ColorSpectrumAlphaSlider.Value = alpha; + } + } + else + { + // Red + if (this.Channel1NumberBox != null) + { + this.Channel1NumberBox.Minimum = 0; + this.Channel1NumberBox.Maximum = 255; + this.Channel1NumberBox.Value = Convert.ToDouble(rgbColor.R); + } + + if (this.Channel1Slider != null) + { + this.Channel1Slider.Minimum = 0; + this.Channel1Slider.Maximum = 255; + this.Channel1Slider.Value = Convert.ToDouble(rgbColor.R); + } + + // Green + if (this.Channel2NumberBox != null) + { + this.Channel2NumberBox.Minimum = 0; + this.Channel2NumberBox.Maximum = 255; + this.Channel2NumberBox.Value = Convert.ToDouble(rgbColor.G); + } + + if (this.Channel2Slider != null) + { + this.Channel2Slider.Minimum = 0; + this.Channel2Slider.Maximum = 255; + this.Channel2Slider.Value = Convert.ToDouble(rgbColor.G); + } + + // Blue + if (this.Channel3NumberBox != null) + { + this.Channel3NumberBox.Minimum = 0; + this.Channel3NumberBox.Maximum = 255; + this.Channel3NumberBox.Value = Convert.ToDouble(rgbColor.B); + } + + if (this.Channel3Slider != null) + { + this.Channel3Slider.Minimum = 0; + this.Channel3Slider.Maximum = 255; + this.Channel3Slider.Value = Convert.ToDouble(rgbColor.B); + } + + // Alpha + if (this.AlphaChannelNumberBox != null) + { + this.AlphaChannelNumberBox.Minimum = 0; + this.AlphaChannelNumberBox.Maximum = 255; + this.AlphaChannelNumberBox.Value = Convert.ToDouble(rgbColor.A); + } + + if (this.AlphaChannelSlider != null) + { + this.AlphaChannelSlider.Minimum = 0; + this.AlphaChannelSlider.Maximum = 255; + this.AlphaChannelSlider.Value = Convert.ToDouble(rgbColor.A); + } + + // Color spectrum alpha + if (this.ColorSpectrumAlphaSlider != null) + { + this.ColorSpectrumAlphaSlider.Minimum = 0; + this.ColorSpectrumAlphaSlider.Maximum = 255; + this.ColorSpectrumAlphaSlider.Value = Convert.ToDouble(rgbColor.A); + } + } + + if (eventsDisconnectedByMethod) + { + this.ConnectEvents(true); + } + } + + return; + } + + /// + /// Sets a new color channel value to the current color. + /// Only the specified color channel will be modified. + /// + /// The color representation of the given channel. + /// The specified color channel to modify. + /// The new color channel value. + private void SetColorChannel( + ColorRepresentation colorRepresentation, + ColorChannel channel, + double newValue) + { + Color oldRgbColor = this.Color; + Color newRgbColor; + HsvColor oldHsvColor; + + if (colorRepresentation == ColorRepresentation.Hsva) + { + // Warning: Always maintain/use HSV information in the saved HSV color + // This avoids loss of precision and drift caused by continuously converting to/from RGB + if (this.savedHsvColor == null) + { + oldHsvColor = oldRgbColor.ToHsv(); + } + else + { + oldHsvColor = this.savedHsvColor.Value; + } + + double hue = oldHsvColor.H; + double saturation = oldHsvColor.S; + double value = oldHsvColor.V; + double alpha = oldHsvColor.A; + + switch (channel) + { + case ColorChannel.Channel1: + { + hue = Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 360); + break; + } + + case ColorChannel.Channel2: + { + saturation = Math.Clamp((double.IsNaN(newValue) ? 0 : newValue) / 100, 0, 1); + break; + } + + case ColorChannel.Channel3: + { + value = Math.Clamp((double.IsNaN(newValue) ? 0 : newValue) / 100, 0, 1); + break; + } + + case ColorChannel.Alpha: + { + // Unlike color channels, default to no transparency + alpha = Math.Clamp((double.IsNaN(newValue) ? 100 : newValue) / 100, 0, 1); + break; + } + } + + newRgbColor = Helpers.ColorHelper.FromHsv( + hue, + saturation, + value, + alpha); + + // Must update HSV color + this.savedHsvColor = new HsvColor() + { + H = hue, + S = saturation, + V = value, + A = alpha + }; + this.savedHsvColorRgbEquivalent = newRgbColor; + } + else + { + byte red = oldRgbColor.R; + byte green = oldRgbColor.G; + byte blue = oldRgbColor.B; + byte alpha = oldRgbColor.A; + + switch (channel) + { + case ColorChannel.Channel1: + { + red = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 255)); + break; + } + + case ColorChannel.Channel2: + { + green = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 255)); + break; + } + + case ColorChannel.Channel3: + { + blue = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 255)); + break; + } + + case ColorChannel.Alpha: + { + // Unlike color channels, default to no transparency + alpha = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 255 : newValue, 0, 255)); + break; + } + } + + newRgbColor = new Color() + { + R = red, + G = green, + B = blue, + A = alpha + }; + + // Must clear saved HSV color + this.savedHsvColor = null; + this.savedHsvColorRgbEquivalent = null; + } + + this.ScheduleColorUpdate(newRgbColor); + return; + } + + /// + /// Updates all channel slider control backgrounds. + /// + private void UpdateChannelSliderBackgrounds() + { + this.UpdateChannelSliderBackground(this.Channel1Slider); + this.UpdateChannelSliderBackground(this.Channel2Slider); + this.UpdateChannelSliderBackground(this.Channel3Slider); + this.UpdateChannelSliderBackground(this.AlphaChannelSlider); + this.UpdateChannelSliderBackground(this.ColorSpectrumAlphaSlider); + this.UpdateChannelSliderBackground(this.ColorSpectrumThirdDimensionSlider); + return; + } + + /// + /// Updates a specific channel slider control background. + /// + /// The color channel slider to update the background for. + private void UpdateChannelSliderBackground(ColorPickerSlider slider) + { + if (slider != null) + { + // Regardless of the active color representation, the sliders always use HSV + // Therefore, always calculate HSV color here + // Warning: Always maintain/use HSV information in the saved HSV color + // This avoids loss of precision and drift caused by continuously converting to/from RGB + if (this.savedHsvColor == null) + { + var rgbColor = this.Color; + + // Must update HSV color + this.savedHsvColor = rgbColor.ToHsv(); + this.savedHsvColorRgbEquivalent = rgbColor; + } + + slider.IsAutoUpdatingEnabled = false; + + if (object.ReferenceEquals(slider, this.Channel1Slider)) + { + slider.ColorChannel = ColorChannel.Channel1; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.Channel2Slider)) + { + slider.ColorChannel = ColorChannel.Channel2; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.Channel3Slider)) + { + slider.ColorChannel = ColorChannel.Channel3; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.AlphaChannelSlider)) + { + slider.ColorChannel = ColorChannel.Alpha; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.ColorSpectrumAlphaSlider)) + { + slider.ColorChannel = ColorChannel.Alpha; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.ColorSpectrumThirdDimensionSlider)) + { + slider.ColorChannel = this.GetActiveColorSpectrumThirdDimension(); + slider.ColorRepresentation = ColorRepresentation.Hsva; // Always HSV + } + + slider.HsvColor = this.savedHsvColor.Value; + slider.UpdateColors(); + } + + return; + } + + /// + /// Sets the default color palette to the control. + /// + private void SetDefaultPalette() + { + this.CustomPalette = new FluentColorPalette(); + + return; + } + + /// + /// Validates and updates the current 'tab' or 'panel' selection. + /// If the currently selected tab is collapsed, the next visible tab will be selected. + /// + private void ValidateSelectedPanel() + { + object? selectedItem = null; + + if (this.ColorPanelSelector != null) + { + if (this.ColorPanelSelector.SelectedItem == null && + this.ColorPanelSelector.Items.Count > 0) + { + // As a failsafe, forcefully select the first item + selectedItem = this.ColorPanelSelector.Items[0]; + } + else + { + selectedItem = this.ColorPanelSelector.SelectedItem; + } + + if (selectedItem is UIElement selectedElement && + selectedElement.Visibility == Visibility.Collapsed) + { + // Select the first visible item instead + foreach (object item in this.ColorPanelSelector.Items) + { + if (item is UIElement element && + element.Visibility == Visibility.Visible) + { + selectedItem = item; + break; + } + } + } + + this.ColorPanelSelector.SelectedItem = selectedItem; + } + + return; + } + private void OnPanelVisibilityChanged(DependencyObject sender, DependencyProperty dp) + { + this.UpdateVisualState(false); + this.ValidateSelectedPanel(); + } + + private void OnDependencyPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + { + if (sender is DependencyObject senderControl) + { + /* Note: ColorProperty is defined in the base class and cannot be used here + * See the OnColorChanged callback below + */ + + if (object.ReferenceEquals(args.Property, CustomPaletteProperty)) + { + IColorPalette palette = this.CustomPalette; + + if (palette != null) + { + this.CustomPaletteColumnCount = palette.ColorCount; + this.CustomPaletteColors.Clear(); + + for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) + { + for (int colorIndex = 0; colorIndex < palette.ColorCount; colorIndex++) + { + this.CustomPaletteColors.Add(palette.GetColor(colorIndex, shadeIndex)); + } + } + } + } + else if (object.ReferenceEquals(args.Property, IsColorPaletteVisibleProperty)) + { + this.UpdateVisualState(false); + this.ValidateSelectedPanel(); + } + } + + return; + } + + /*************************************************************************************** + * + * Color Update Timer + * + ***************************************************************************************/ + + private void StartDispatcherQueueTimer() + { + this.dispatcherQueueTimer = DispatcherQueue.GetForCurrentThread().CreateTimer(); + this.dispatcherQueueTimer.Interval = new TimeSpan(0, 0, 0, 0, ColorUpdateInterval); + this.dispatcherQueueTimer.Tick += DispatcherQueueTimer_Tick; + this.dispatcherQueueTimer.Start(); + + return; + } + + private void StopDispatcherQueueTimer() + { + if (this.dispatcherQueueTimer != null) + { + this.dispatcherQueueTimer.Stop(); + } + + return; + } + + private void DispatcherQueueTimer_Tick(object sender, object e) + { + if (this.updatedRgbColor != null) + { + var newColor = this.updatedRgbColor.Value; + + // Clear first to avoid timing issues if it takes longer than the timer interval to set the new color + this.updatedRgbColor = null; + + // An equality check here is important + // Without it, OnColorChanged would continuously be invoked and preserveHsvColor overwritten when not wanted + if (object.Equals(newColor, this.GetValue(ColorProperty)) == false) + { + // Disable events here so the color update isn't repeated as other controls in the UI are updated through binding. + // For example, the Spectrum should be bound to Color, as soon as Color is changed here the Spectrum is updated. + // Then however, the ColorSpectrum.ColorChanged event would fire which would schedule a new color update -- + // with the same color. This causes several problems: + // 1. Layout cycle that may crash the app + // 2. A performance hit recalculating for no reason + // 3. preserveHsvColor gets overwritten unexpectedly by the ColorChanged handler + this.ConnectEvents(false); + this.SetValue(ColorProperty, newColor); + this.ConnectEvents(true); + } + } + + return; + } + + /*************************************************************************************** + * + * Callbacks + * + ***************************************************************************************/ + + /// + /// Callback for when the dependency property value changes. + /// + private void OnColorChanged(DependencyObject d, DependencyProperty e) + { + // TODO: Coerce the value if Alpha is disabled, is this handled in the base ColorPicker? + if ((this.savedHsvColor != null) && + (object.Equals(d.GetValue(e), this.savedHsvColorRgbEquivalent) == false)) + { + // The color was updated from an unknown source + // The RGB and HSV colors are no longer in sync so the HSV color must be cleared + this.savedHsvColor = null; + this.savedHsvColorRgbEquivalent = null; + } + + this.UpdateColorControlValues(); + this.UpdateChannelSliderBackgrounds(); + + return; + } + + /*************************************************************************************** + * + * Event Handling + * + ***************************************************************************************/ + + /// + /// Event handler for when the control has finished loaded. + /// + private void ColorPickerButton_Loaded(object sender, RoutedEventArgs e) + { + // Available but not currently used + return; + } + + /// + /// Event handler for when a color channel slider is loaded. + /// This will draw an initial background. + /// + private void ChannelSlider_Loaded(object sender, RoutedEventArgs e) + { + if (sender is ColorPickerSlider slider) + { + this.UpdateChannelSliderBackground(slider); + } + return; + } + + /// + /// Event handler to draw checkered backgrounds on-demand as controls are loaded. + /// + private async void CheckeredBackgroundBorder_Loaded(object sender, RoutedEventArgs e) + { + if (sender is Border border) + { + await ColorPickerRenderingHelpers.UpdateBorderBackgroundWithCheckerAsync( + border, + CheckerBackgroundColor); + } + + } + + /// + /// Event handler for when the list of custom palette colors is changed. + /// + private void CustomPaletteColors_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // Available but not currently used + return; + } + + /// + /// Event handler for when the color spectrum color is changed. + /// This occurs when the user presses on the spectrum to select a new color. + /// + private void ColorSpectrum_ColorChanged(ColorSpectrum sender, Microsoft.UI.Xaml.Controls.ColorChangedEventArgs args) + { + // It is OK in this case to use the RGB representation + this.ScheduleColorUpdate(this.ColorSpectrumControl.Color); + return; + } + + /// + /// Event handler for when the color spectrum is focused. + /// This is used only to work around some bugs that cause usability problems. + /// + private void ColorSpectrum_GotFocus(object sender, RoutedEventArgs e) + { + Color rgbColor = this.ColorSpectrumControl.Color; + + /* If this control has a color that is currently empty (#00000000), + * selecting a new color directly in the spectrum will fail. This is + * a bug in the color spectrum. Selecting a new color in the spectrum will + * keep zero for all channels (including alpha and the third dimension). + * + * In practice this means a new color cannot be selected using the spectrum + * until both the alpha and third dimension slider are raised above zero. + * This is extremely user unfriendly and must be corrected as best as possible. + * + * In order to work around this, detect when the color spectrum has selected + * a new color and then automatically set the alpha and third dimension + * channel to maximum. However, the color spectrum has a second bug, the + * ColorChanged event is never raised if the color is empty. This prevents + * automatically setting the other channels where it normally should be done + * (in the ColorChanged event). + * + * In order to work around this second bug, the GotFocus event is used + * to detect when the spectrum is engaged by the user. It's somewhat equivalent + * to ColorChanged for this purpose. Then when the GotFocus event is fired + * set the alpha and third channel values to maximum. The problem here is that + * the GotFocus event does not have access to the new color that was selected + * in the spectrum. It is not available due to the afore mentioned bug or due to + * timing. This means the best that can be done is to just set a 'neutral' + * color such as white. + * + * There is still a small usability issue with this as it requires two + * presses to set a color. That's far better than having to slide up both + * sliders though. + * + * 1. If the color is empty, the first press on the spectrum will set white + * and ignore the pressed color on the spectrum + * 2. The second press on the spectrum will be correctly handled. + * + */ + + // In the future Color.IsEmpty will hopefully be added to UWP + if (IsColorEmpty(rgbColor)) + { + /* The following code may be used in the future if ever the selected color is available + + Color newColor = this.ColorSpectrum.Color; + HsvColor newHsvColor = newColor.ToHsv(); + + switch (this.GetActiveColorSpectrumThirdDimension()) + { + case ColorChannel.Channel1: + { + newColor = Microsoft.Toolkit.Uwp.Helpers.ColorHelper.FromHsv + ( + 360.0, + newHsvColor.S, + newHsvColor.V, + 100.0 + ); + break; + } + + case ColorChannel.Channel2: + { + newColor = Microsoft.Toolkit.Uwp.Helpers.ColorHelper.FromHsv + ( + newHsvColor.H, + 100.0, + newHsvColor.V, + 100.0 + ); + break; + } + + case ColorChannel.Channel3: + { + newColor = Microsoft.Toolkit.Uwp.Helpers.ColorHelper.FromHsv + ( + newHsvColor.H, + newHsvColor.S, + 100.0, + 100.0 + ); + break; + } + } + */ + + this.ScheduleColorUpdate(Colors.White); + } + else if (rgbColor.A == 0x00) + { + // As an additional usability improvement, reset alpha to maximum when the spectrum is used. + // The color spectrum has no alpha channel and it is much more intuitive to do this for the user + // especially if the picker was initially set with Colors.Transparent. + this.ScheduleColorUpdate(Color.FromArgb(0xFF, rgbColor.R, rgbColor.G, rgbColor.B)); + } + + return; + } + + /// + /// Event handler for when the selected color representation changes. + /// This will convert between RGB and HSV. + /// + private void ColorModeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (this.ColorModeComboBox.SelectedIndex == 1) + { + this.SetActiveColorRepresentation(ColorRepresentation.Hsva); + } + else + { + this.SetActiveColorRepresentation(ColorRepresentation.Rgba); + } + + this.UpdateColorControlValues(); + this.UpdateChannelSliderBackgrounds(); + + return; + } + + /// + /// Event handler for when the color previewer requests a new color. + /// + private void ColorPreviewer_ColorChangeRequested(object? sender, HsvColor hsvColor) + { + Color rgbColor = Helpers.ColorHelper.FromHsv(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); + + // Regardless of the active color model, the previewer always uses HSV + // Therefore, always calculate HSV color here + // Warning: Always maintain/use HSV information in the saved HSV color + // This avoids loss of precision and drift caused by continuously converting to/from RGB + this.savedHsvColor = hsvColor; + this.savedHsvColorRgbEquivalent = rgbColor; + + this.ScheduleColorUpdate(rgbColor); + + return; + } + + /// + /// Event handler for when a key is pressed within the Hex RGB value TextBox. + /// This is used to trigger a re-evaluation of the color based on the TextBox value. + /// + private void HexInputTextBox_KeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == Windows.System.VirtualKey.Enter) + { + try + { + ColorToHexConverter converter = new ColorToHexConverter(); + this.Color = (Color)converter.ConvertBack(((TextBox)sender).Text, typeof(TextBox), null, null); + } + catch + { + // Reset hex value + this.UpdateColorControlValues(); + this.UpdateChannelSliderBackgrounds(); + } + } + + return; + } + + /// + /// Event handler for when the Hex RGB value TextBox looses focus. + /// This is used to trigger a re-evaluation of the color based on the TextBox value. + /// + private void HexInputTextBox_LostFocus(object sender, RoutedEventArgs e) + { + try + { + ColorToHexConverter converter = new ColorToHexConverter(); + this.Color = (Color)converter.ConvertBack(((TextBox)sender).Text, typeof(TextBox), null, null); + } + catch + { + // Reset hex value + this.UpdateColorControlValues(); + this.UpdateChannelSliderBackgrounds(); + } + + return; + } + + /// + /// Event handler for when the value within one of the channel NumberBoxes is changed. + /// + private void ChannelNumberBox_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args) + { + double senderValue = sender.Value; + + if (object.ReferenceEquals(sender, this.Channel1NumberBox)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel1, senderValue); + } + else if (object.ReferenceEquals(sender, this.Channel2NumberBox)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel2, senderValue); + } + else if (object.ReferenceEquals(sender, this.Channel3NumberBox)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel3, senderValue); + } + else if (object.ReferenceEquals(sender, this.AlphaChannelNumberBox)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, senderValue); + } + + return; + } + + /// + /// Event handler for when the value within one of the channel Sliders is changed. + /// + private void ChannelSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e) + { + double senderValue = (sender as Slider)?.Value ?? double.NaN; + + if (object.ReferenceEquals(sender, this.Channel1Slider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel1, senderValue); + } + else if (object.ReferenceEquals(sender, this.Channel2Slider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel2, senderValue); + } + else if (object.ReferenceEquals(sender, this.Channel3Slider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel3, senderValue); + } + else if (object.ReferenceEquals(sender, this.AlphaChannelSlider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, senderValue); + } + else if (object.ReferenceEquals(sender, this.ColorSpectrumAlphaSlider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, senderValue); + } + else if (object.ReferenceEquals(sender, this.ColorSpectrumThirdDimensionSlider)) + { + // Regardless of the active color representation, the spectrum is always HSV + this.SetColorChannel(ColorRepresentation.Hsva, this.GetActiveColorSpectrumThirdDimension(), senderValue); + } + + return; + } +} diff --git a/components/ColorPicker/src/ColorPicker.xaml b/components/ColorPicker/src/ColorPicker.xaml new file mode 100644 index 00000000..284eb7b2 --- /dev/null +++ b/components/ColorPicker/src/ColorPicker.xaml @@ -0,0 +1,987 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RGB + HSV + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Visible + + + + + + + + + + + + + + + + + + + + + + + + + + + +