[Color Picker] remove and export multiple colors from history (#16475)

* Allow to export and delete multiple colors

* Allow to export and delete multiple colors
This commit is contained in:
mshtang 2022-03-07 12:47:42 +01:00 committed by GitHub
parent e8363a3be1
commit 81f61630cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 331 additions and 28 deletions

View File

@ -79,9 +79,13 @@ namespace ColorPicker.Helpers
/// <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)}";
{
const string hexFormat = "x2";
return $"{color.R.ToString(hexFormat, CultureInfo.InvariantCulture)}"
+ $"{color.G.ToString(hexFormat, CultureInfo.InvariantCulture)}"
+ $"{color.B.ToString(hexFormat, CultureInfo.InvariantCulture)}";
}
/// <summary>
/// Return a <see cref="string"/> representation of a HSB color
@ -109,9 +113,12 @@ namespace ColorPicker.Helpers
private static string ColorToFloat(Color color)
{
var (red, green, blue) = ColorHelper.ConvertToDouble(color);
var precision = 2;
const int precision = 2;
const string floatFormat = "0.##";
return $"({Math.Round(red, precision).ToString("0.##", CultureInfo.InvariantCulture)}f, {Math.Round(green, precision).ToString("0.##", CultureInfo.InvariantCulture)}f, {Math.Round(blue, precision).ToString("0.##", CultureInfo.InvariantCulture)}f, 1f)";
return $"({Math.Round(red, precision).ToString(floatFormat, CultureInfo.InvariantCulture)}f"
+ $", {Math.Round(green, precision).ToString(floatFormat, CultureInfo.InvariantCulture)}f"
+ $", {Math.Round(blue, precision).ToString(floatFormat, CultureInfo.InvariantCulture)}f, 1f)";
}
/// <summary>

View File

@ -0,0 +1,131 @@
// 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.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Windows.Media;
using ColorPicker.Models;
namespace ColorPicker.Helpers
{
internal enum GroupExportedColorsBy
{
Color,
Format,
}
internal static class SerializationHelper
{
public static Dictionary<string, Dictionary<string, string>> ConvertToDesiredColorFormats(
IList colorsToExport,
IEnumerable<ColorFormatModel> colorRepresentations,
GroupExportedColorsBy method)
{
var colors = new Dictionary<string, Dictionary<string, string>>();
var colorFormatModels = colorRepresentations.ToList();
var i = 1;
switch (method)
{
case GroupExportedColorsBy.Color:
{
foreach (Color color in (IList)colorsToExport)
{
var tmp = new Dictionary<string, string>();
foreach (var colorFormatModel in colorFormatModels)
{
var colorInSpecificFormat = colorFormatModel.Convert(color);
if (colorFormatModel.FormatName == "HEX")
{
colorInSpecificFormat = "#" + colorInSpecificFormat;
}
tmp.Add(
colorFormatModel.FormatName,
#pragma warning disable CA1308 // Normalize strings to uppercase
colorInSpecificFormat.Replace(
colorFormatModel.FormatName.ToLower(CultureInfo.InvariantCulture),
string.Empty,
StringComparison.InvariantCultureIgnoreCase));
#pragma warning restore CA1308 // Normalize strings to uppercase
}
colors.Add($"color{i++}", tmp);
}
}
break;
case GroupExportedColorsBy.Format:
{
foreach (var colorFormatModel in colorFormatModels)
{
var tmp = new Dictionary<string, string>();
i = 1;
foreach (Color color in (IList)colorsToExport)
{
var colorInSpecificFormat = colorFormatModel.Convert(color);
if (colorFormatModel.FormatName == "HEX")
{
colorInSpecificFormat = "#" + colorInSpecificFormat;
}
tmp.Add(
$"color{i++}",
#pragma warning disable CA1308 // Normalize strings to uppercase
colorInSpecificFormat.Replace(
colorFormatModel.FormatName.ToLower(CultureInfo.InvariantCulture),
string.Empty,
StringComparison.InvariantCultureIgnoreCase));
#pragma warning restore CA1308 // Normalize strings to uppercase
}
colors.Add(colorFormatModel.FormatName, tmp);
}
}
break;
}
return colors;
}
public static string ToTxt(this Dictionary<string, Dictionary<string, string>> source, char separator)
{
var res = string.Empty;
foreach (var (key, val) in source)
{
res += $"{key}{separator}";
res = val.Aggregate(res, (current, pair) =>
{
var (p, v) = pair;
// if grouped by format, add a space between color* and its value, to avoid illegibility:
// decimal;color1 12345678;color2 23456789;color11 13579246
if (key == "Decimal")
{
v = " " + v;
}
return current + $"{p}{v}{separator}";
});
res = res.TrimEnd(separator) + System.Environment.NewLine;
}
return res;
}
public static string ToJson(this Dictionary<string, Dictionary<string, string>> source, bool indented = true)
{
var options = new JsonSerializerOptions
{
WriteIndented = indented,
};
return JsonSerializer.Serialize(source, options);
}
}
}

View File

@ -19,7 +19,7 @@ namespace ColorPicker.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@ -159,6 +159,33 @@ namespace ColorPicker.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Export.
/// </summary>
public static string Export_by {
get {
return ResourceManager.GetString("Export_by", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Grouped by color.
/// </summary>
public static string Export_by_color {
get {
return ResourceManager.GetString("Export_by_color", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Grouped by format.
/// </summary>
public static string Export_by_format {
get {
return ResourceManager.GetString("Export_by_format", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Green value.
/// </summary>

View File

@ -173,6 +173,18 @@
<value>Remove</value>
<comment>Tooltip that shows that the user can remove an item from a list</comment>
</data>
<data name="Export_by" xml:space="preserve">
<value>Export</value>
<comment>Tooltip that shows the user can export the selected items to a file</comment>
</data>
<data name="Export_by_color" xml:space="preserve">
<value>Grouped by color</value>
<comment>Tooltip that shows the user can export the selected items grouped by color to a file</comment>
</data>
<data name="Export_by_format" xml:space="preserve">
<value>Grouped by format</value>
<comment>Tooltip that shows the user can export the selected items grouped by format to a file</comment>
</data>
<data name="Saturation_slider" xml:space="preserve">
<value>Saturation slider</value>
<comment>Tool tip that appears when hovering over a slider that represents the color saturation (from HSV)</comment>

View File

@ -29,6 +29,8 @@ namespace ColorPicker.Telemetry
public bool EditorColorAdjusted { get; set; }
public bool EditorColorsExported { get; set; }
public bool EditorSimilarColorPicked { get; set; }
public bool EditorHistoryColorPicked { get; set; }

View File

@ -20,7 +20,11 @@ namespace ColorPicker.ViewModelContracts
ICommand OpenSettingsCommand { get; }
ICommand RemoveColorCommand { get; }
ICommand RemoveColorsCommand { get; }
ICommand ExportColorsGroupedByColorCommand { get; }
ICommand ExportColorsGroupedByFormatCommand { get; }
ObservableCollection<ColorFormatModel> ColorRepresentations { get; }

View File

@ -3,10 +3,13 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Drawing.Drawing2D;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Windows.Input;
using System.Windows.Media;
@ -16,6 +19,7 @@ using ColorPicker.Models;
using ColorPicker.Settings;
using ColorPicker.ViewModelContracts;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using Microsoft.Win32;
namespace ColorPicker.ViewModels
{
@ -33,10 +37,17 @@ namespace ColorPicker.ViewModels
{
OpenColorPickerCommand = new RelayCommand(() => OpenColorPickerRequested?.Invoke(this, EventArgs.Empty));
OpenSettingsCommand = new RelayCommand(() => OpenSettingsRequested?.Invoke(this, EventArgs.Empty));
RemoveColorCommand = new RelayCommand(DeleteSelectedColor);
RemoveColorsCommand = new RelayCommand(DeleteSelectedColors);
ExportColorsGroupedByColorCommand = new RelayCommand(ExportSelectedColorsByColor);
ExportColorsGroupedByFormatCommand = new RelayCommand(ExportSelectedColorsByFormat);
SelectedColorChangedCommand = new RelayCommand((newColor) =>
{
if (ColorsHistory.Contains((Color)newColor))
{
ColorsHistory.Remove((Color)newColor);
}
ColorsHistory.Insert(0, (Color)newColor);
SelectedColorIndex = 0;
});
@ -54,7 +65,11 @@ namespace ColorPicker.ViewModels
public ICommand OpenSettingsCommand { get; }
public ICommand RemoveColorCommand { get; }
public ICommand RemoveColorsCommand { get; }
public ICommand ExportColorsGroupedByColorCommand { get; }
public ICommand ExportColorsGroupedByFormatCommand { get; }
public ICommand SelectedColorChangedCommand { get; }
@ -133,15 +148,99 @@ namespace ColorPicker.ViewModels
}
}
private void DeleteSelectedColor()
private void DeleteSelectedColors(object selectedColors)
{
// select new color on the same index if possible, otherwise the last one
var indexToSelect = SelectedColorIndex == ColorsHistory.Count - 1 ? ColorsHistory.Count - 2 : SelectedColorIndex;
ColorsHistory.RemoveAt(SelectedColorIndex);
SelectedColorIndex = indexToSelect;
var colorsToRemove = ((IList)selectedColors).OfType<Color>().ToList();
var indicesToRemove = colorsToRemove.Select(color => ColorsHistory.IndexOf(color)).ToList();
foreach (var color in colorsToRemove)
{
ColorsHistory.Remove(color);
}
SelectedColorIndex = ComputeWhichIndexToSelectAfterDeletion(colorsToRemove.Count + ColorsHistory.Count, indicesToRemove);
SessionEventHelper.Event.EditorHistoryColorRemoved = true;
}
private void ExportSelectedColorsByColor(object selectedColors)
{
ExportColors(selectedColors, GroupExportedColorsBy.Color);
}
private void ExportSelectedColorsByFormat(object selectedColors)
{
ExportColors(selectedColors, GroupExportedColorsBy.Format);
}
private void ExportColors(object colorsToExport, GroupExportedColorsBy method)
{
var colors = SerializationHelper.ConvertToDesiredColorFormats((IList)colorsToExport, ColorRepresentations, method);
var dialog = new SaveFileDialog
{
Title = "Save selected colors to",
Filter = "Text Files (*.txt)|*.txt|Json Files (*.json)|*.json",
};
if (dialog.ShowDialog() == true)
{
var extension = Path.GetExtension(dialog.FileName);
var contentToWrite = extension.ToUpperInvariant() switch
{
".TXT" => colors.ToTxt(';'),
".JSON" => colors.ToJson(),
_ => string.Empty
};
File.WriteAllText(dialog.FileName, contentToWrite);
SessionEventHelper.Event.EditorColorsExported = true;
}
}
// Will select the closest color to the last selected one in color history
private static int ComputeWhichIndexToSelectAfterDeletion(int colorsCount, List<int> indicesToRemove)
{
var newIndices = Enumerable.Range(0, colorsCount).ToList();
foreach (var index in indicesToRemove)
{
newIndices[index] = -1;
}
var appearancesOfMinusOne = 0;
for (var i = 0; i < newIndices.Count; i++)
{
if (newIndices[i] < 0)
{
appearancesOfMinusOne++;
continue;
}
newIndices[i] -= appearancesOfMinusOne;
}
var lastSelectedIndex = indicesToRemove.Last();
for (int i = lastSelectedIndex - 1, j = lastSelectedIndex + 1; ; i--, j++)
{
if (j < newIndices.Count && newIndices[j] != -1)
{
return newIndices[j];
}
if (i >= 0 && newIndices[i] != -1)
{
return newIndices[i];
}
if (j >= newIndices.Count && i < 0)
{
break;
}
}
return -1;
}
private void SetupAllColorRepresentations()
{
_allColorRepresentations.Add(
@ -149,7 +248,7 @@ namespace ColorPicker.ViewModels
{
FormatName = ColorRepresentationType.HEX.ToString(),
#pragma warning disable CA1304 // Specify CultureInfo
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HEX).ToLower(); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HEX).ToLower(),
#pragma warning restore CA1304 // Specify CultureInfo
});
@ -157,76 +256,76 @@ namespace ColorPicker.ViewModels
new ColorFormatModel()
{
FormatName = ColorRepresentationType.RGB.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.RGB); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.RGB),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.HSL.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HSL); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HSL),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.HSV.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HSV); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HSV),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.CMYK.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CMYK); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CMYK),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.HSB.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HSB); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HSB),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.HSI.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HSI); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HSI),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.HWB.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HWB); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.HWB),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.NCol.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.NCol); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.NCol),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.CIELAB.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIELAB); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIELAB),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.CIEXYZ.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIEXYZ); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIEXYZ),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.VEC4.ToString(),
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.VEC4); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.VEC4),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = "Decimal",
Convert = (Color color) => { return ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.DecimalValue); },
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.DecimalValue),
});
_userSettings.VisibleColorFormats.CollectionChanged += VisibleColorFormats_CollectionChanged;

View File

@ -33,15 +33,36 @@
SelectedIndex="{Binding SelectedColorIndex}"
ItemContainerStyle="{DynamicResource ColorHistoryListViewStyle}"
IsItemClickEnabled="True"
SelectionMode="Extended"
ItemClick="HistoryColors_ItemClick">
<ui:ListView.ContextMenu>
<ContextMenu Visibility="{Binding ColorsHistory.Count, Converter={StaticResource numberToVisibilityConverter}}">
<MenuItem Header="{x:Static p:Resources.Remove}"
Command="{Binding RemoveColorCommand}">
Command="{Binding RemoveColorsCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}, Path=PlacementTarget.SelectedItems}">
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xE107;" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{x:Static p:Resources.Export_by}" >
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xEDE1;"/>
</MenuItem.Icon>
<MenuItem Header="{x:Static p:Resources.Export_by_color}"
Command="{Binding ExportColorsGroupedByColorCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}, Path=PlacementTarget.SelectedItems}">
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xE790;"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{x:Static p:Resources.Export_by_format}"
Command="{Binding ExportColorsGroupedByFormatCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}, Path=PlacementTarget.SelectedItems}" >
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xE943;"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</ContextMenu>
</ui:ListView.ContextMenu>
<ui:ListView.ItemTemplate>