Skip to content

Commit 121c6c0

Browse files
crramirezCopilotCopilot
authored
Add RGB hex color preview to Advanced Paste clipboard history (#43990)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This pull request adds support for recognizing and displaying clipboard items that are valid RGB hex color codes (such as `#FFBFAB` or `#abc`) in the Advanced Paste module. It introduces logic to detect hex color strings, converts them to color values, and updates the UI to show a color preview for these items. The changes also include comprehensive unit tests for the new functionality. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #43538 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments **Clipboard color detection and conversion:** * Added `ClipboardItemHelper.IsRgbHexColor` method using a compiled regex to identify valid hex color strings in clipboard text. [[1]](diffhunk://#diff-7429196ad30cd0bce57b102669da4dc13d43a09579e99ceac7cc0f7dc101cd2bR62-R86) [[2]](diffhunk://#diff-7429196ad30cd0bce57b102669da4dc13d43a09579e99ceac7cc0f7dc101cd2bR112-R114) * Introduced `HexColorConverterHelper.ConvertHexColorToRgb` utility to convert hex color strings to `Windows.UI.Color`, handling both 3-digit and 6-digit formats. **UI enhancements for color previews:** * Updated `ClipboardHistoryItemPreviewControl` to include a color preview grid that displays an ellipse filled with the detected color and the color code as text, using the new `HexColorToBrushConverter`. [[1]](diffhunk://#diff-2ed6014d4c17037b9cd0ab397e40b9069b1e7fe47a700673f34e8217d78124d5R29-R48) [[2]](diffhunk://#diff-2ed6014d4c17037b9cd0ab397e40b9069b1e7fe47a700673f34e8217d78124d5R14) [[3]](diffhunk://#diff-0c26c92697f6bb38fa40160fc8b18f0876ddc8d828a510034411001aa2e05063R1-R28) * Modified logic in `ClipboardHistoryItemPreviewControl.xaml.cs` to ensure color previews are shown only for detected color items and to adjust visibility of text and glyph previews accordingly. **Unit tests for color detection and conversion:** * Added unit tests for hex color conversion (`HexColorToColorConverterTests.cs`) and color detection logic (`ClipboardItemHelperTests.cs`) to verify correct behavior for valid, invalid, and edge-case inputs. [[1]](diffhunk://#diff-d81d997d5fb414f1563c31c38681113aaa9c847ef05bb77662d30bd1310d6b8eR1-R61) [[2]](diffhunk://#diff-185e8954ca6f061bf5d60d0c61ac6cfd87bd1a48ebda11a8172e3496a050fe85R1-R36) <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed * Copied to the clipboard a color encoded text like: #FFBFAB * Opened Advanced Paste and noticed the color: <img width="467" height="309" alt="image" src="https://github.com/user-attachments/assets/6cedce89-9833-4efb-abf9-3cfe8e8f32f0" /> --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: crramirez <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent e68526b commit 121c6c0

File tree

7 files changed

+217
-3
lines changed

7 files changed

+217
-3
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using AdvancedPaste.Converters;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
using Windows.UI;
8+
9+
namespace AdvancedPaste.UnitTests.ConvertersTests;
10+
11+
[TestClass]
12+
public sealed class HexColorToColorConverterTests
13+
{
14+
[TestMethod]
15+
public void TestConvert_ValidSixDigitHex_ReturnsColor()
16+
{
17+
Color? result = HexColorConverterHelper.ConvertHexColorToRgb("#FFBFAB");
18+
Assert.IsNotNull(result);
19+
20+
var color = (Windows.UI.Color)result;
21+
Assert.AreEqual(255, color.R);
22+
Assert.AreEqual(191, color.G);
23+
Assert.AreEqual(171, color.B);
24+
Assert.AreEqual(255, color.A);
25+
}
26+
27+
[TestMethod]
28+
public void TestConvert_ValidThreeDigitHex_ReturnsColor()
29+
{
30+
Color? result = HexColorConverterHelper.ConvertHexColorToRgb("#abc");
31+
Assert.IsNotNull(result);
32+
33+
var color = (Windows.UI.Color)result;
34+
35+
// #abc should expand to #aabbcc
36+
Assert.AreEqual(170, color.R); // 0xaa
37+
Assert.AreEqual(187, color.G); // 0xbb
38+
Assert.AreEqual(204, color.B); // 0xcc
39+
Assert.AreEqual(255, color.A);
40+
}
41+
42+
[TestMethod]
43+
public void TestConvert_NullOrEmpty_ReturnsNull()
44+
{
45+
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(null));
46+
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(string.Empty));
47+
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(" "));
48+
}
49+
50+
[TestMethod]
51+
public void TestConvert_InvalidHex_ReturnsNull()
52+
{
53+
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb("#GGGGGG"));
54+
Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb("#12345"));
55+
}
56+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using AdvancedPaste.Helpers;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace AdvancedPaste.UnitTests.HelpersTests;
9+
10+
[TestClass]
11+
public sealed class ClipboardItemHelperTests
12+
{
13+
[TestMethod]
14+
[DataRow("#FFBFAB", true)]
15+
[DataRow("#000000", true)]
16+
[DataRow("#FFFFFF", true)]
17+
[DataRow("#fff", true)]
18+
[DataRow("#abc", true)]
19+
[DataRow("#123456", true)]
20+
[DataRow("#AbCdEf", true)]
21+
[DataRow("FFBFAB", false)] // Missing #
22+
[DataRow("#GGGGGG", false)] // Invalid hex characters
23+
[DataRow("#12345", false)] // Wrong length
24+
[DataRow("#1234567", false)] // Too long
25+
[DataRow("", false)]
26+
[DataRow(null, false)]
27+
[DataRow(" #FFF ", true)] // Whitespace should be trimmed
28+
[DataRow("Not a color", false)]
29+
[DataRow("#", false)]
30+
[DataRow("##FFFFFF", false)]
31+
public void TestIsRgbHexColor(string input, bool expected)
32+
{
33+
bool result = ClipboardItemHelper.IsRgbHexColor(input);
34+
Assert.AreEqual(expected, result, $"IsRgbHexColor(\"{input}\") should return {expected}");
35+
}
36+
}

src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
mc:Ignorable="d">
1212
<UserControl.Resources>
1313
<converters:DateTimeToFriendlyStringConverter x:Key="DateTimeToFriendlyStringConverter" />
14+
<converters:HexColorToBrushConverter x:Key="HexColorToBrushConverter" />
1415
<tkconverters:BoolToVisibilityConverter x:Name="BoolToVisibilityConverter" />
1516
</UserControl.Resources>
1617
<Grid ColumnSpacing="12">
@@ -25,6 +26,26 @@
2526
Source="{x:Bind ClipboardItem.Image, Mode=OneWay}"
2627
Stretch="UniformToFill"
2728
Visibility="{x:Bind HasImage, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
29+
<!-- Color preview with text -->
30+
<Grid Visibility="{x:Bind HasColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
31+
<Grid.ColumnDefinitions>
32+
<ColumnDefinition Width="Auto" />
33+
<ColumnDefinition Width="*" />
34+
</Grid.ColumnDefinitions>
35+
<Ellipse
36+
Grid.Column="0"
37+
Width="8"
38+
Height="8"
39+
Margin="8,0,8,0"
40+
Fill="{x:Bind ClipboardItem.Content, Mode=OneWay, Converter={StaticResource HexColorToBrushConverter}}" />
41+
<TextBlock
42+
Grid.Column="1"
43+
VerticalAlignment="Center"
44+
FontSize="10"
45+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
46+
Text="{x:Bind ClipboardItem.Content, Mode=OneWay}"
47+
TextWrapping="NoWrap" />
48+
</Grid>
2849
<!-- Text preview -->
2950
<TextBlock
3051
Margin="8,0,0,0"

src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ public ClipboardItem ClipboardItem
3838

3939
public bool HasImage => ContentImage is not null;
4040

41-
public bool HasText => !string.IsNullOrEmpty(ContentText) && !HasImage;
41+
public bool HasText => !string.IsNullOrEmpty(ContentText) && !HasImage && !HasColor;
4242

43-
public bool HasGlyph => !HasImage && !HasText && !string.IsNullOrEmpty(IconGlyph);
43+
public bool HasGlyph => !HasImage && !HasText && !HasColor && !string.IsNullOrEmpty(IconGlyph);
44+
45+
public bool HasColor => ClipboardItemHelper.IsRgbHexColor(ContentText);
4446

4547
public ClipboardHistoryItemPreviewControl()
4648
{
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace AdvancedPaste.Converters
6+
{
7+
public static class HexColorConverterHelper
8+
{
9+
public static Windows.UI.Color? ConvertHexColorToRgb(string hexColor)
10+
{
11+
try
12+
{
13+
// Remove # if present
14+
var cleanHex = hexColor.TrimStart('#');
15+
16+
// Expand 3-digit hex to 6-digit (#ABC -> #AABBCC)
17+
if (cleanHex.Length == 3)
18+
{
19+
cleanHex = $"{cleanHex[0]}{cleanHex[0]}{cleanHex[1]}{cleanHex[1]}{cleanHex[2]}{cleanHex[2]}";
20+
}
21+
22+
if (cleanHex.Length == 6)
23+
{
24+
var r = System.Convert.ToByte(cleanHex.Substring(0, 2), 16);
25+
var g = System.Convert.ToByte(cleanHex.Substring(2, 2), 16);
26+
var b = System.Convert.ToByte(cleanHex.Substring(4, 2), 16);
27+
28+
return Windows.UI.Color.FromArgb(255, r, g, b);
29+
}
30+
}
31+
catch
32+
{
33+
// Invalid color format - return null
34+
}
35+
36+
return null;
37+
}
38+
}
39+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using Microsoft.UI.Xaml.Data;
7+
using Microsoft.UI.Xaml.Media;
8+
9+
namespace AdvancedPaste.Converters
10+
{
11+
public sealed partial class HexColorToBrushConverter : IValueConverter
12+
{
13+
public object ConvertBack(object value, Type targetType, object parameter, string language)
14+
=> throw new NotSupportedException();
15+
16+
public object Convert(object value, Type targetType, object parameter, string language)
17+
{
18+
if (value is not string hexColor || string.IsNullOrWhiteSpace(hexColor))
19+
{
20+
return null;
21+
}
22+
23+
Windows.UI.Color? color = HexColorConverterHelper.ConvertHexColorToRgb(hexColor);
24+
25+
return color != null ? new SolidColorBrush((Windows.UI.Color)color) : null;
26+
}
27+
}
28+
}

src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Text.RegularExpressions;
67
using System.Threading.Tasks;
78
using AdvancedPaste.Models;
89
using Microsoft.UI.Xaml.Media.Imaging;
910
using Windows.ApplicationModel.DataTransfer;
1011

1112
namespace AdvancedPaste.Helpers
1213
{
13-
internal static class ClipboardItemHelper
14+
internal static partial class ClipboardItemHelper
1415
{
16+
// Compiled regex for better performance when checking multiple clipboard items
17+
private static readonly Regex HexColorRegex = HexColorCompiledRegex();
18+
1519
/// <summary>
1620
/// Creates a ClipboardItem from current clipboard data.
1721
/// </summary>
@@ -55,6 +59,31 @@ public static async Task<ClipboardItem> CreateFromCurrentClipboardAsync(
5559
return clipboardItem;
5660
}
5761

62+
/// <summary>
63+
/// Checks if text is a valid RGB hex color (e.g., #FFBFAB or #fff).
64+
/// </summary>
65+
public static bool IsRgbHexColor(string text)
66+
{
67+
if (text == null)
68+
{
69+
return false;
70+
}
71+
72+
string trimmedText = text.Trim();
73+
if (trimmedText.Length > 7)
74+
{
75+
return false;
76+
}
77+
78+
if (string.IsNullOrWhiteSpace(trimmedText))
79+
{
80+
return false;
81+
}
82+
83+
// Match #RGB or #RRGGBB format (case-insensitive)
84+
return HexColorRegex.IsMatch(trimmedText);
85+
}
86+
5887
/// <summary>
5988
/// Creates a BitmapImage from clipboard data.
6089
/// </summary>
@@ -80,5 +109,8 @@ private static async Task<BitmapImage> TryCreateBitmapImageAsync(DataPackageView
80109

81110
return null;
82111
}
112+
113+
[GeneratedRegex(@"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")]
114+
private static partial Regex HexColorCompiledRegex();
83115
}
84116
}

0 commit comments

Comments
 (0)