terminal/src/cascadia/TerminalSettingsEditor/ColorSchemes.cpp
gabrielconl d0d3cc6a75
Terminal color picker tweaks (#10219)
The flyout wasn't very polished, so I did some adjustments.
It's all visual changes, functionality should be the same.

* made the flyout use OverlayCornerRadius and 16px padding (to match WinUI 2.6)
* changed ColorPicker to muxc:ColorPicker for new styles (the color schemes picker too)
* changed "Custom" Button into a ToggleButton
  * no longer needs ellipsis - localization files should be updated
* OK button was moved to the right and uses accent color
* adjusted margins and padding
* tweaked the color boxes to _look_ like the ones in color schemes

![collapsednew](https://user-images.githubusercontent.com/84711285/119713282-33cfcf80-be6a-11eb-9ad9-d18a97b1058a.png) ![expandednew](https://user-images.githubusercontent.com/84711285/119713295-35999300-be6a-11eb-8423-c1c03526b23a.png)

## Validation Steps Performed

* Color picker in settings UI still works ✔️
* Color picker for tabs still works ✔️
2021-06-10 15:49:52 +00:00

438 lines
18 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ColorSchemes.h"
#include "ColorTableEntry.g.cpp"
#include "ColorSchemes.g.cpp"
#include "ColorSchemesPageNavigationState.g.cpp"
#include <LibraryResources.h>
using namespace winrt;
using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Navigation;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Media;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Microsoft::UI::Xaml::Controls;
namespace winrt
{
namespace MUX = Microsoft::UI::Xaml;
namespace WUX = Windows::UI::Xaml;
}
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
// The first 8 entries of the color table are non-bright colors, whereas the rest are bright.
static constexpr uint8_t ColorTableDivider{ 8 };
static constexpr std::wstring_view ForegroundColorTag{ L"Foreground" };
static constexpr std::wstring_view BackgroundColorTag{ L"Background" };
static constexpr std::wstring_view CursorColorTag{ L"CursorColor" };
static constexpr std::wstring_view SelectionBackgroundColorTag{ L"SelectionBackground" };
static const std::array<hstring, 16> TableColorNames = {
RS_(L"ColorScheme_Black/Header"),
RS_(L"ColorScheme_Red/Header"),
RS_(L"ColorScheme_Green/Header"),
RS_(L"ColorScheme_Yellow/Header"),
RS_(L"ColorScheme_Blue/Header"),
RS_(L"ColorScheme_Purple/Header"),
RS_(L"ColorScheme_Cyan/Header"),
RS_(L"ColorScheme_White/Header"),
RS_(L"ColorScheme_BrightBlack/Header"),
RS_(L"ColorScheme_BrightRed/Header"),
RS_(L"ColorScheme_BrightGreen/Header"),
RS_(L"ColorScheme_BrightYellow/Header"),
RS_(L"ColorScheme_BrightBlue/Header"),
RS_(L"ColorScheme_BrightPurple/Header"),
RS_(L"ColorScheme_BrightCyan/Header"),
RS_(L"ColorScheme_BrightWhite/Header")
};
static const std::array<std::wstring, 9> InBoxSchemes = {
L"Campbell",
L"Campbell Powershell",
L"Vintage",
L"One Half Dark",
L"One Half Light",
L"Solarized Dark",
L"Solarized Light",
L"Tango Dark",
L"Tango Light"
};
ColorSchemes::ColorSchemes() :
_ColorSchemeList{ single_threaded_observable_vector<Model::ColorScheme>() },
_CurrentNonBrightColorTable{ single_threaded_observable_vector<Editor::ColorTableEntry>() },
_CurrentBrightColorTable{ single_threaded_observable_vector<Editor::ColorTableEntry>() }
{
InitializeComponent();
Automation::AutomationProperties::SetName(ColorSchemeComboBox(), RS_(L"ColorScheme_Name/Header"));
Automation::AutomationProperties::SetFullDescription(ColorSchemeComboBox(), RS_(L"ColorScheme_Name/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
ToolTipService::SetToolTip(ColorSchemeComboBox(), box_value(RS_(L"ColorScheme_Name/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")));
Automation::AutomationProperties::SetName(RenameButton(), RS_(L"Rename/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
Automation::AutomationProperties::SetName(NameBox(), RS_(L"ColorScheme_Name/Header"));
Automation::AutomationProperties::SetFullDescription(NameBox(), RS_(L"ColorScheme_Name/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
ToolTipService::SetToolTip(NameBox(), box_value(RS_(L"ColorScheme_Name/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip")));
Automation::AutomationProperties::SetName(RenameAcceptButton(), RS_(L"RenameAccept/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
Automation::AutomationProperties::SetName(RenameCancelButton(), RS_(L"RenameCancel/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"));
Automation::AutomationProperties::SetName(AddNewButton(), RS_(L"ColorScheme_AddNewButton/Text"));
Automation::AutomationProperties::SetName(DeleteButton(), RS_(L"ColorScheme_DeleteButton/Text"));
}
void ColorSchemes::OnNavigatedTo(const NavigationEventArgs& e)
{
_State = e.Parameter().as<Editor::ColorSchemesPageNavigationState>();
_UpdateColorSchemeList();
// Initialize our color table view model with 16 dummy colors
// so that on a ColorScheme selection change, we can loop through
// each ColorTableEntry and just change its color. Performing a
// clear and 16 appends doesn't seem to update the color pickers
// very accurately.
for (uint8_t i = 0; i < TableColorNames.size(); ++i)
{
const auto& entry{ winrt::make<ColorTableEntry>(i, Windows::UI::Color{ 0, 0, 0, 0 }) };
if (i < ColorTableDivider)
{
_CurrentNonBrightColorTable.Append(entry);
}
else
{
_CurrentBrightColorTable.Append(entry);
}
}
_CurrentForegroundColor = winrt::make<ColorTableEntry>(ForegroundColorTag, Windows::UI::Color{ 0, 0, 0, 0 });
_CurrentBackgroundColor = winrt::make<ColorTableEntry>(BackgroundColorTag, Windows::UI::Color{ 0, 0, 0, 0 });
_CurrentCursorColor = winrt::make<ColorTableEntry>(CursorColorTag, Windows::UI::Color{ 0, 0, 0, 0 });
_CurrentSelectionBackgroundColor = winrt::make<ColorTableEntry>(SelectionBackgroundColorTag, Windows::UI::Color{ 0, 0, 0, 0 });
// Try to look up the scheme that was navigated to. If we find it, immediately select it.
const std::wstring lastNameFromNav{ _State.LastSelectedScheme() };
const auto it = std::find_if(begin(_ColorSchemeList),
end(_ColorSchemeList),
[&lastNameFromNav](const auto& scheme) { return scheme.Name() == lastNameFromNav; });
if (it != end(_ColorSchemeList))
{
auto scheme = *it;
ColorSchemeComboBox().SelectedItem(scheme);
}
// populate color table grid
const auto colorLabelStyle{ Resources().Lookup(winrt::box_value(L"ColorLabelStyle")).as<Windows::UI::Xaml::Style>() };
const auto colorControlStyle{ Resources().Lookup(winrt::box_value(L"ColorControlStyle")).as<Windows::UI::Xaml::Style>() };
const auto colorTableEntryTemplate{ Resources().Lookup(winrt::box_value(L"ColorTableEntryTemplate")).as<DataTemplate>() };
auto setupColorControl = [colorTableEntryTemplate, colorControlStyle, colorTableGrid{ ColorTableGrid() }](const auto&& colorRef, const uint32_t& row, const uint32_t& col) {
ContentControl colorControl{};
colorControl.ContentTemplate(colorTableEntryTemplate);
colorControl.Style(colorControlStyle);
Data::Binding binding{};
binding.Source(colorRef);
binding.Mode(Data::BindingMode::TwoWay);
colorControl.SetBinding(ContentControl::ContentProperty(), binding);
colorTableGrid.Children().Append(colorControl);
Grid::SetRow(colorControl, row);
Grid::SetColumn(colorControl, col);
};
for (uint32_t row = 0; row < ColorTableGrid().RowDefinitions().Size(); ++row)
{
// color label
TextBlock label{};
label.Text(TableColorNames[row]);
label.Style(colorLabelStyle);
ColorTableGrid().Children().Append(label);
Grid::SetRow(label, row);
Grid::SetColumn(label, 0);
// regular color
setupColorControl(_CurrentNonBrightColorTable.GetAt(row), row, 1);
// bright color
setupColorControl(_CurrentBrightColorTable.GetAt(row), row, 2);
}
}
// Function Description:
// - Called when a different color scheme is selected. Updates our current
// color scheme and updates our currently modifiable color table.
// Arguments:
// - args: The selection changed args that tells us what's the new color scheme selected.
// Return Value:
// - <none>
void ColorSchemes::ColorSchemeSelectionChanged(IInspectable const& /*sender*/,
SelectionChangedEventArgs const& args)
{
// Update the color scheme this page is modifying
const auto colorScheme{ args.AddedItems().GetAt(0).try_as<Model::ColorScheme>() };
CurrentColorScheme(colorScheme);
_UpdateColorTable(colorScheme);
_State.LastSelectedScheme(colorScheme.Name());
// Set the text disclaimer for the text box
hstring disclaimer{};
const std::wstring schemeName{ colorScheme.Name() };
if (std::find(std::begin(InBoxSchemes), std::end(InBoxSchemes), schemeName) != std::end(InBoxSchemes))
{
// load disclaimer for in-box profiles
disclaimer = RS_(L"ColorScheme_DeleteButtonDisclaimerInBox");
}
DeleteButtonDisclaimer().Text(disclaimer);
// Update the state of the page
_PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"CanDeleteCurrentScheme" });
IsRenaming(false);
}
// Function Description:
// - Updates the list of all color schemes available to choose from.
// Arguments:
// - <none>
// Return Value:
// - <none>
void ColorSchemes::_UpdateColorSchemeList()
{
// Surprisingly, though this is called every time we navigate to the page,
// the list does not keep growing on each navigation.
const auto& colorSchemeMap{ _State.Settings().GlobalSettings().ColorSchemes() };
for (const auto& pair : colorSchemeMap)
{
_ColorSchemeList.Append(pair.Value());
}
}
// Function Description:
// - Called when a ColorPicker control has selected a new color. This is specifically
// called by color pickers assigned to a color table entry. It takes the index
// that's been stuffed in the Tag property of the color picker and uses it
// to update the color table accordingly.
// Arguments:
// - sender: the color picker that raised this event.
// - args: the args that contains the new color that was picked.
// Return Value:
// - <none>
void ColorSchemes::ColorPickerChanged(IInspectable const& sender,
MUX::Controls::ColorChangedEventArgs const& args)
{
const til::color newColor{ args.NewColor() };
if (const auto& picker{ sender.try_as<MUX::Controls::ColorPicker>() })
{
if (const auto& tag{ picker.Tag() })
{
if (const auto index{ tag.try_as<uint8_t>() })
{
CurrentColorScheme().SetColorTableEntry(*index, newColor);
if (index < ColorTableDivider)
{
_CurrentNonBrightColorTable.GetAt(*index).Color(newColor);
}
else
{
_CurrentBrightColorTable.GetAt(*index - ColorTableDivider).Color(newColor);
}
}
else if (const auto stringTag{ tag.try_as<hstring>() })
{
if (stringTag == ForegroundColorTag)
{
CurrentColorScheme().Foreground(newColor);
_CurrentForegroundColor.Color(newColor);
}
else if (stringTag == BackgroundColorTag)
{
CurrentColorScheme().Background(newColor);
_CurrentBackgroundColor.Color(newColor);
}
else if (stringTag == CursorColorTag)
{
CurrentColorScheme().CursorColor(newColor);
_CurrentCursorColor.Color(newColor);
}
else if (stringTag == SelectionBackgroundColorTag)
{
CurrentColorScheme().SelectionBackground(newColor);
_CurrentSelectionBackgroundColor.Color(newColor);
}
}
}
}
}
bool ColorSchemes::CanDeleteCurrentScheme() const
{
if (const auto& scheme{ CurrentColorScheme() })
{
// Only allow this color scheme to be deleted if it's not provided in-box
const std::wstring myName{ scheme.Name() };
return std::find(std::begin(InBoxSchemes), std::end(InBoxSchemes), myName) == std::end(InBoxSchemes);
}
return false;
}
void ColorSchemes::DeleteConfirmation_Click(IInspectable const& /*sender*/, RoutedEventArgs const& /*e*/)
{
const auto schemeName{ CurrentColorScheme().Name() };
_State.Settings().GlobalSettings().RemoveColorScheme(schemeName);
// This ensures that the JSON is updated with "Campbell", because the color scheme was deleted
_State.Settings().UpdateColorSchemeReferences(schemeName, L"Campbell");
const auto removedSchemeIndex{ ColorSchemeComboBox().SelectedIndex() };
if (static_cast<uint32_t>(removedSchemeIndex) < _ColorSchemeList.Size() - 1)
{
// select same index
ColorSchemeComboBox().SelectedIndex(removedSchemeIndex + 1);
}
else
{
// select last color scheme (avoid out of bounds error)
ColorSchemeComboBox().SelectedIndex(removedSchemeIndex - 1);
}
_ColorSchemeList.RemoveAt(removedSchemeIndex);
DeleteButton().Flyout().Hide();
}
void ColorSchemes::AddNew_Click(IInspectable const& /*sender*/, RoutedEventArgs const& /*e*/)
{
// Give the new scheme a distinct name
const hstring schemeName{ fmt::format(L"Color Scheme {}", _State.Settings().GlobalSettings().ColorSchemes().Size() + 1) };
Model::ColorScheme scheme{ schemeName };
// Add the new color scheme
_State.Settings().GlobalSettings().AddColorScheme(scheme);
// Update current page
_ColorSchemeList.Append(scheme);
ColorSchemeComboBox().SelectedItem(scheme);
}
// Function Description:
// - Pre-populates/focuses the name TextBox, updates the UI
// Arguments:
// - <unused>
// Return Value:
// - <none>
void ColorSchemes::Rename_Click(IInspectable const& /*sender*/, RoutedEventArgs const& /*e*/)
{
NameBox().Text(CurrentColorScheme().Name());
IsRenaming(true);
NameBox().Focus(FocusState::Programmatic);
NameBox().SelectAll();
}
void ColorSchemes::RenameAccept_Click(IInspectable const& /*sender*/, RoutedEventArgs const& /*e*/)
{
_RenameCurrentScheme(NameBox().Text());
RenameButton().Focus(FocusState::Programmatic);
}
void ColorSchemes::RenameCancel_Click(IInspectable const& /*sender*/, RoutedEventArgs const& /*e*/)
{
IsRenaming(false);
RenameErrorTip().IsOpen(false);
RenameButton().Focus(FocusState::Programmatic);
}
void ColorSchemes::NameBox_PreviewKeyDown(IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e)
{
if (e.OriginalKey() == winrt::Windows::System::VirtualKey::Enter)
{
_RenameCurrentScheme(NameBox().Text());
e.Handled(true);
}
else if (e.OriginalKey() == winrt::Windows::System::VirtualKey::Escape)
{
IsRenaming(false);
RenameErrorTip().IsOpen(false);
e.Handled(true);
}
ColorSchemeComboBox().Focus(FocusState::Programmatic);
}
void ColorSchemes::_RenameCurrentScheme(hstring newName)
{
// check if different name is already in use
const auto oldName{ CurrentColorScheme().Name() };
if (newName != oldName && _State.Settings().GlobalSettings().ColorSchemes().HasKey(newName))
{
// open the error tip
RenameErrorTip().Target(NameBox());
RenameErrorTip().IsOpen(true);
// focus the name box
NameBox().Focus(FocusState::Programmatic);
NameBox().SelectAll();
return;
}
// update the settings model
CurrentColorScheme().Name(newName);
_State.Settings().GlobalSettings().RemoveColorScheme(oldName);
_State.Settings().GlobalSettings().AddColorScheme(CurrentColorScheme());
_State.Settings().UpdateColorSchemeReferences(oldName, newName);
// update the UI
RenameErrorTip().IsOpen(false);
CurrentColorScheme().Name(newName);
IsRenaming(false);
// The color scheme is renamed appropriately, but the ComboBox still shows the old name (until you open it)
// We need to manually force the ComboBox to refresh itself.
const auto selectedIndex{ ColorSchemeComboBox().SelectedIndex() };
ColorSchemeComboBox().SelectedIndex((selectedIndex + 1) % ColorSchemeList().Size());
ColorSchemeComboBox().SelectedIndex(selectedIndex);
}
// Function Description:
// - Updates the currently modifiable color table based on the given current color scheme.
// Arguments:
// - colorScheme: the color scheme to retrieve the color table from
// Return Value:
// - <none>
void ColorSchemes::_UpdateColorTable(const Model::ColorScheme& colorScheme)
{
for (uint8_t i = 0; i < TableColorNames.size(); ++i)
{
til::color currentColor{ colorScheme.Table()[i] };
if (i < ColorTableDivider)
{
_CurrentNonBrightColorTable.GetAt(i).Color(currentColor);
}
else
{
_CurrentBrightColorTable.GetAt(i - ColorTableDivider).Color(currentColor);
}
}
_CurrentForegroundColor.Color(til::color{ colorScheme.Foreground() });
_CurrentBackgroundColor.Color(til::color{ colorScheme.Background() });
_CurrentCursorColor.Color(til::color{ colorScheme.CursorColor() });
_CurrentSelectionBackgroundColor.Color(til::color{ colorScheme.SelectionBackground() });
}
ColorTableEntry::ColorTableEntry(uint8_t index, Windows::UI::Color color)
{
Name(TableColorNames[index]);
Tag(winrt::box_value<uint8_t>(index));
Color(color);
}
ColorTableEntry::ColorTableEntry(std::wstring_view tag, Windows::UI::Color color)
{
Name(LocalizedNameForEnumName(L"ColorScheme_", tag, L"Text"));
Tag(winrt::box_value(tag));
Color(color);
}
}