diff --git a/src/cascadia/TerminalApp/SuggestionsControl.cpp b/src/cascadia/TerminalApp/SuggestionsControl.cpp index 83694267a6d..9d2feada98f 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.cpp +++ b/src/cascadia/TerminalApp/SuggestionsControl.cpp @@ -8,6 +8,7 @@ #include #include "SuggestionsControl.g.cpp" +#include "../../types/inc/utils.hpp" using namespace winrt; using namespace winrt::TerminalApp; @@ -19,6 +20,8 @@ using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace std::chrono_literals; + namespace winrt::TerminalApp::implementation { SuggestionsControl::SuggestionsControl() @@ -262,8 +265,8 @@ namespace winrt::TerminalApp::implementation // - // Return Value: // - - void SuggestionsControl::_selectedCommandChanged(const IInspectable& /*sender*/, - const Windows::UI::Xaml::RoutedEventArgs& /*args*/) + winrt::fire_and_forget SuggestionsControl::_selectedCommandChanged(const IInspectable& /*sender*/, + const Windows::UI::Xaml::RoutedEventArgs& /*args*/) { const auto selectedCommand = _filteredActionsView().SelectedItem(); const auto filteredCommand{ selectedCommand.try_as() }; @@ -281,9 +284,83 @@ namespace winrt::TerminalApp::implementation { if (const auto actionPaletteItem{ filteredCommand.Item().try_as() }) { - PreviewAction.raise(*this, actionPaletteItem.Command()); + const auto& cmd = actionPaletteItem.Command(); + PreviewAction.raise(*this, cmd); + + const auto description{ cmd.Description() }; + if (!description.empty()) + { + // If it's already open, then just re-target it and update the content immediately. + if (DescriptionTip().IsOpen()) + { + _openTooltip(cmd); + } + else + { + // Otherwise, wait a bit before opening it. + co_await winrt::resume_after(200ms); + co_await wil::resume_foreground(Dispatcher()); + _openTooltip(cmd); + DescriptionTip().IsOpen(true); + } + } + else + { + // If there's no description, then just close the tooltip. + DescriptionTip().IsOpen(false); + } + } + } + } + + winrt::fire_and_forget SuggestionsControl::_openTooltip(Command cmd) + { + const auto description{ cmd.Description() }; + + if (!cmd.Description().empty()) + { + DescriptionTip().Target(SelectedItem()); + DescriptionTip().Title(cmd.Name()); + + // If you try to put a newline in the Subtitle, it'll _immediately + // close the tooltip_. Instead, we'll need to build up the text as a + // series of runs, and put them in the content. + + _toolTipContent().Inlines().Clear(); + + // First, replace all "\r\n" with "\n" + std::wstring filtered = description.c_str(); + + // replace all "\r\n" with "\n" in `filtered` + std::wstring::size_type pos = 0; + while ((pos = filtered.find(L"\r\n", pos)) != std::wstring::npos) + { + filtered.erase(pos, 1); + } + + // Split the filtered description on '\n` + const auto lines = ::Microsoft::Console::Utils::SplitString(filtered.c_str(), L'\n'); + // For each line, build a Run + LineBreak, and add them to the text + // block + for (const auto& line : lines) + { + if (line.empty()) + continue; + Documents::Run textRun; + textRun.Text(winrt::hstring{ line }); + _toolTipContent().Inlines().Append(textRun); + _toolTipContent().Inlines().Append(Documents::LineBreak{}); } + + // TODO! These were all feigned attempts to allow us to focus the content of the teachingtip. + // We may want to keep IsTextSelectionEnabled in the XAML. + // + // I also have no idea if the FullDescriptionProperty thing worked at all. + _toolTipContent().AllowFocusOnInteraction(true); + _toolTipContent().IsTextSelectionEnabled(true); + DescriptionTip().SetValue(Automation::AutomationProperties::FullDescriptionProperty(), winrt::box_value(description)); } + co_return; } void SuggestionsControl::_previewKeyDownHandler(const IInspectable& /*sender*/, @@ -377,7 +454,9 @@ namespace winrt::TerminalApp::implementation // If the user types a character while the menu (not in palette mode) // is open, then dismiss ourselves. That way, when you type a character, // we'll instead send it to the TermControl. - if (_mode == SuggestionsMode::Menu && !e.Handled()) + if (_mode == SuggestionsMode::Menu && + !e.Handled() && + key != VirtualKey::F6) { _dismissPalette(); } @@ -444,7 +523,21 @@ namespace winrt::TerminalApp::implementation return; } + const auto& popups = Media::VisualTreeHelper::GetOpenPopupsForXamlRoot(root); + const auto numPopups = popups.Size(); + // TODO! I think _we_ are the first popup. + if (numPopups > 1) + { + // TODO! a test. If theres a popup, don't dismiss. + return; + } + + // TODO! if the teaching tip is closed, re-focus the suggestions list + auto focusedElementOrAncestor = Input::FocusManager::GetFocusedElement(root).try_as(); + // TODO! if there wasn't a focused element in the main root, look + // instead underneath the teachingtip. left as an exersize to figure out + // _how_ to do that. while (focusedElementOrAncestor) { if (focusedElementOrAncestor == *this) @@ -946,6 +1039,7 @@ namespace winrt::TerminalApp::implementation void SuggestionsControl::_close() { Visibility(Visibility::Collapsed); + DescriptionTip().IsOpen(false); // Clear the text box each time we close the dialog. This is consistent with VsCode. _searchBox().Text(L""); diff --git a/src/cascadia/TerminalApp/SuggestionsControl.h b/src/cascadia/TerminalApp/SuggestionsControl.h index 252332493b7..f7af9faa4d2 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.h +++ b/src/cascadia/TerminalApp/SuggestionsControl.h @@ -109,7 +109,11 @@ namespace winrt::TerminalApp::implementation void _listItemClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::ItemClickEventArgs& e); void _listItemSelectionChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e); - void _selectedCommandChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); + + winrt::fire_and_forget _selectedCommandChanged(const Windows::Foundation::IInspectable& sender, + const Windows::UI::Xaml::RoutedEventArgs& args); + + winrt::fire_and_forget _openTooltip(Microsoft::Terminal::Settings::Model::Command cmd); void _moveBackButtonClicked(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs&); void _updateCurrentNestedCommands(const winrt::Microsoft::Terminal::Settings::Model::Command& parentCommand); diff --git a/src/cascadia/TerminalApp/SuggestionsControl.xaml b/src/cascadia/TerminalApp/SuggestionsControl.xaml index a59e2e342d9..bb0803ba8e8 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.xaml +++ b/src/cascadia/TerminalApp/SuggestionsControl.xaml @@ -206,6 +206,15 @@ ItemsSource="{x:Bind FilteredActions}" SelectionChanged="_listItemSelectionChanged" SelectionMode="Single" /> + + + + + diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index e2eec5c95b0..c45a5e260ec 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -27,6 +27,7 @@ static constexpr std::string_view ArgsKey{ "args" }; static constexpr std::string_view IterateOnKey{ "iterateOn" }; static constexpr std::string_view CommandsKey{ "commands" }; static constexpr std::string_view KeysKey{ "keys" }; +static constexpr std::string_view DescriptionKey{ "description" }; static constexpr std::string_view ProfileNameToken{ "${profile.name}" }; static constexpr std::string_view ProfileIconToken{ "${profile.icon}" }; @@ -40,6 +41,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { auto command{ winrt::make_self() }; command->_name = _name; + command->_Description = _Description; command->_ActionAndArgs = *get_self(_ActionAndArgs)->Copy(); command->_keyMappings = _keyMappings; command->_iconPath = _iconPath; @@ -254,7 +256,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto nested = false; JsonUtils::GetValueForKey(json, IterateOnKey, result->_IterateOn); - + JsonUtils::GetValueForKey(json, DescriptionKey, result->_Description); // For iterable commands, we'll make another pass at parsing them once // the json is patched. So ignore parsing sub-commands for now. Commands // will only be marked iterable on the first pass. @@ -406,7 +408,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Json::Value cmdJson{ Json::ValueType::objectValue }; JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath); JsonUtils::SetValueForKey(cmdJson, NameKey, _name); - + JsonUtils::SetValueForKey(cmdJson, DescriptionKey, _Description); if (_ActionAndArgs) { cmdJson[JsonKey(ActionKey)] = ActionAndArgs::ToJson(_ActionAndArgs); @@ -426,6 +428,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation // First iteration also writes icon and name JsonUtils::SetValueForKey(cmdJson, IconKey, _iconPath); JsonUtils::SetValueForKey(cmdJson, NameKey, _name); + JsonUtils::SetValueForKey(cmdJson, DescriptionKey, _Description); } if (_ActionAndArgs) @@ -654,8 +657,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const auto parseElement = [&](const auto& element) { winrt::hstring completionText; winrt::hstring listText; + winrt::hstring tooltipText; JsonUtils::GetValueForKey(element, "CompletionText", completionText); JsonUtils::GetValueForKey(element, "ListItemText", listText); + JsonUtils::GetValueForKey(element, "ToolTip", tooltipText); auto args = winrt::make_self( winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"), @@ -668,6 +673,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto c = winrt::make_self(); c->_name = listText; c->_ActionAndArgs = actionAndArgs; + c->_Description = tooltipText; // Try to assign a sensible icon based on the result type. These are // roughly chosen to align with the icons in diff --git a/src/cascadia/TerminalSettingsModel/Command.h b/src/cascadia/TerminalSettingsModel/Command.h index b001c795671..2cd41baa48f 100644 --- a/src/cascadia/TerminalSettingsModel/Command.h +++ b/src/cascadia/TerminalSettingsModel/Command.h @@ -70,6 +70,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation WINRT_PROPERTY(ExpandCommandType, IterateOn, ExpandCommandType::None); WINRT_PROPERTY(Model::ActionAndArgs, ActionAndArgs); + WINRT_PROPERTY(winrt::hstring, Description, L""); private: Json::Value _originalJson; diff --git a/src/cascadia/TerminalSettingsModel/Command.idl b/src/cascadia/TerminalSettingsModel/Command.idl index 934df64d6cb..dde93f062c3 100644 --- a/src/cascadia/TerminalSettingsModel/Command.idl +++ b/src/cascadia/TerminalSettingsModel/Command.idl @@ -36,6 +36,8 @@ namespace Microsoft.Terminal.Settings.Model Command(); String Name { get; }; + String Description { get; }; + ActionAndArgs ActionAndArgs { get; }; Microsoft.Terminal.Control.KeyChord Keys { get; }; void RegisterKey(Microsoft.Terminal.Control.KeyChord keys);