Add buttons for selecting commands, output to context menu (#15020)

Adds a "Select command" and a "Select output" entry to the right-click
context menu when the user has shell integration enabled. This lets the
user quickly right-click on a command and select the entire commandline
or all of its output.

This was a "I'm waiting for reviews" sorta idea. Seemed like a
reasonable combination of features. Related to #13445, #11000.

Tested manually.

---------

Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>
This commit is contained in:
Mike Griese
2023-04-25 09:43:49 -05:00
committed by GitHub
parent 2cfd73d819
commit 0d1540bbd2
12 changed files with 249 additions and 19 deletions

View File

@@ -2182,10 +2182,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
}
void ControlCore::_selectSpan(til::point_span s)
{
const auto bufferSize{ _terminal->GetTextBuffer().GetSize() };
bufferSize.DecrementInBounds(s.end);
auto lock = _terminal->LockForWriting();
_terminal->SelectNewRegion(s.start, s.end);
_renderer->TriggerSelection();
}
void ControlCore::SelectCommand(const bool goUp)
{
const til::point start = HasSelection() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) :
_terminal->GetTextBuffer().GetCursor().GetPosition();
std::optional<DispatchTypes::ScrollMark> nearest{ std::nullopt };
const auto& marks{ _terminal->GetScrollMarks() };
@@ -2217,13 +2228,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
const auto start = nearest->end;
auto end = *nearest->commandEnd;
const auto bufferSize{ _terminal->GetTextBuffer().GetSize() };
bufferSize.DecrementInBounds(end);
auto lock = _terminal->LockForWriting();
_terminal->SelectNewRegion(start, end);
_renderer->TriggerSelection();
_selectSpan(til::point_span{ start, end });
}
}
@@ -2231,6 +2236,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
const til::point start = HasSelection() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) :
_terminal->GetTextBuffer().GetCursor().GetPosition();
std::optional<DispatchTypes::ScrollMark> nearest{ std::nullopt };
const auto& marks{ _terminal->GetScrollMarks() };
@@ -2256,13 +2262,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
const auto start = *nearest->commandEnd;
auto end = *nearest->outputEnd;
const auto bufferSize{ _terminal->GetTextBuffer().GetSize() };
bufferSize.DecrementInBounds(end);
auto lock = _terminal->LockForWriting();
_terminal->SelectNewRegion(start, end);
_renderer->TriggerSelection();
_selectSpan(til::point_span{ start, end });
}
}
@@ -2301,4 +2301,110 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
}
}
void ControlCore::AnchorContextMenu(const til::point viewportRelativeCharacterPosition)
{
// viewportRelativeCharacterPosition is relative to the current
// viewport, so adjust for that:
_contextMenuBufferPosition = _terminal->GetViewport().Origin() + viewportRelativeCharacterPosition;
}
void ControlCore::_contextMenuSelectMark(
const til::point& pos,
const std::function<bool(const DispatchTypes::ScrollMark&)>& filter,
const std::function<til::point_span(const DispatchTypes::ScrollMark&)>& getSpan)
{
// Do nothing if the caller didn't give us a way to get the span to select for this mark.
if (!getSpan)
{
return;
}
const auto& marks{ _terminal->GetScrollMarks() };
for (auto&& m : marks)
{
// If the caller gave us a way to filter marks, check that now.
// This can be used to filter to only marks that have a command, or output.
if (filter && filter(m))
{
continue;
}
// If they clicked _anywhere_ in the mark...
const auto [markStart, markEnd] = m.GetExtent();
if (markStart <= pos &&
markEnd >= pos)
{
// ... select the part of the mark the caller told us about.
_selectSpan(getSpan(m));
// And quick bail
return;
}
}
}
void ControlCore::ContextMenuSelectCommand()
{
_contextMenuSelectMark(
_contextMenuBufferPosition,
[](const DispatchTypes::ScrollMark& m) -> bool { return !m.HasCommand(); },
[](const DispatchTypes::ScrollMark& m) { return til::point_span{ m.end, *m.commandEnd }; });
}
void ControlCore::ContextMenuSelectOutput()
{
_contextMenuSelectMark(
_contextMenuBufferPosition,
[](const DispatchTypes::ScrollMark& m) -> bool { return !m.HasOutput(); },
[](const DispatchTypes::ScrollMark& m) { return til::point_span{ *m.commandEnd, *m.outputEnd }; });
}
bool ControlCore::_clickedOnMark(
const til::point& pos,
const std::function<bool(const DispatchTypes::ScrollMark&)>& filter)
{
// Don't show this if the click was on the selection
if (_terminal->IsSelectionActive() &&
_terminal->GetSelectionAnchor() <= pos &&
_terminal->GetSelectionEnd() >= pos)
{
return false;
}
// DO show this if the click was on a mark with a command
const auto& marks{ _terminal->GetScrollMarks() };
for (auto&& m : marks)
{
if (filter && filter(m))
{
continue;
}
const auto [start, end] = m.GetExtent();
if (start <= pos &&
end >= pos)
{
return true;
}
}
// Didn't click on a mark with a command - don't show.
return false;
}
// Method Description:
// * Don't show this if the click was on the _current_ selection
// * Don't show this if the click wasn't on a mark with at least a command
// * Otherwise yea, show it.
bool ControlCore::ShouldShowSelectCommand()
{
// Relies on the anchor set in AnchorContextMenu
return _clickedOnMark(_contextMenuBufferPosition,
[](const DispatchTypes::ScrollMark& m) -> bool { return !m.HasCommand(); });
}
// Method Description:
// * Same as ShouldShowSelectCommand, but with the mark needing output
bool ControlCore::ShouldShowSelectOutput()
{
// Relies on the anchor set in AnchorContextMenu
return _clickedOnMark(_contextMenuBufferPosition,
[](const DispatchTypes::ScrollMark& m) -> bool { return !m.HasOutput(); });
}
}

View File

@@ -152,8 +152,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void ClearMark();
void ClearAllMarks();
void ScrollToMark(const Control::ScrollToMarkDirection& direction);
void SelectCommand(const bool goUp);
void SelectOutput(const bool goUp);
void ContextMenuSelectCommand();
void ContextMenuSelectOutput();
#pragma endregion
#pragma region ITerminalInput
@@ -220,6 +224,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
uint64_t OwningHwnd();
void OwningHwnd(uint64_t owner);
void AnchorContextMenu(til::point viewportRelativeCharacterPosition);
bool ShouldShowSelectCommand();
bool ShouldShowSelectOutput();
RUNTIME_SETTING(double, Opacity, _settings->Opacity());
RUNTIME_SETTING(bool, UseAcrylic, _settings->UseAcrylic());
@@ -304,6 +313,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
std::unique_ptr<til::throttled_func_trailing<>> _updatePatternLocations;
std::shared_ptr<ThrottledFuncTrailing<Control::ScrollPositionChangedArgs>> _updateScrollBar;
til::point _contextMenuBufferPosition{ 0, 0 };
void _setupDispatcherAndCallbacks();
bool _setFontSizeUnderLock(float fontSize);
@@ -349,6 +360,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation
bool _isBackgroundTransparent();
void _focusChanged(bool focused);
void _selectSpan(til::point_span s);
void _contextMenuSelectMark(
const til::point& pos,
const std::function<bool(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark&)>& filter,
const std::function<til::point_span(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark&)>& getSpan);
bool _clickedOnMark(const til::point& pos, const std::function<bool(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark&)>& filter);
inline bool _IsClosing() const noexcept
{
#ifndef NDEBUG

View File

@@ -142,6 +142,11 @@ namespace Microsoft.Terminal.Control
void ColorSelection(SelectionColor fg, SelectionColor bg, Microsoft.Terminal.Core.MatchMode matchMode);
void ContextMenuSelectCommand();
void ContextMenuSelectOutput();
Boolean ShouldShowSelectCommand();
Boolean ShouldShowSelectOutput();
event FontSizeChangedEventArgs FontSizeChanged;
event Windows.Foundation.TypedEventHandler<Object, CopyToClipboardEventArgs> CopyToClipboard;

View File

@@ -310,6 +310,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
if (_core->Settings().RightClickContextMenu())
{
// Let the core know we're about to open a menu here. It has
// some separate conditional logic based on _where_ the user
// wanted to open the menu.
_core->AnchorContextMenu(terminalPosition);
auto contextArgs = winrt::make<ContextMenuRequestedEventArgs>(til::point{ pixelPosition }.to_winrt_point());
_ContextMenuRequestedHandlers(*this, contextArgs);
}

View File

@@ -240,4 +240,36 @@ Please either install the missing font or choose another one.</value>
<value>Find</value>
<comment>The tooltip for a button for searching for the selected text</comment>
</data>
<data name="SelectCommandButton.Label" xml:space="preserve">
<value>Select command</value>
<comment>The label of a button for selecting all of the text of a command</comment>
</data>
<data name="SelectCommandButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Select command</value>
<comment>The tooltip for a button for selecting all of the text of a command</comment>
</data>
<data name="SelectOutputButton.Label" xml:space="preserve">
<value>Select output</value>
<comment>The label of a button for selecting all of a command's output</comment>
</data>
<data name="SelectOutputButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Select output</value>
<comment>The tooltip for a button for selecting all of a command's output</comment>
</data>
<data name="SelectCommandWithSelectionButton.Label" xml:space="preserve">
<value>Select command</value>
<comment>The label of a button for selecting all of the text of a command</comment>
</data>
<data name="SelectCommandWithSelectionButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Select command</value>
<comment>The tooltip for a button for selecting all of the text of a command</comment>
</data>
<data name="SelectOutputWithSelectionButton.Label" xml:space="preserve">
<value>Select output</value>
<comment>The label of a button for selecting all of a command's output</comment>
</data>
<data name="SelectOutputWithSelectionButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Select output</value>
<comment>The tooltip for a button for selecting all of a command's output</comment>
</data>
</root>

View File

@@ -3350,6 +3350,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const auto pos = (absolutePointerPos - absoluteWindowOrigin - controlOrigin).to_winrt_point();
myOption.Position(pos);
// The "Select command" and "Select output" buttons should only be
// visible if shell integration is actually turned on.
const auto shouldShowSelectCommand{ _core.ShouldShowSelectCommand() };
const auto shouldShowSelectOutput{ _core.ShouldShowSelectOutput() };
SelectCommandButton().Visibility(shouldShowSelectCommand ? Visibility::Visible : Visibility::Collapsed);
SelectOutputButton().Visibility(shouldShowSelectOutput ? Visibility::Visible : Visibility::Collapsed);
SelectCommandWithSelectionButton().Visibility(shouldShowSelectCommand ? Visibility::Visible : Visibility::Collapsed);
SelectOutputWithSelectionButton().Visibility(shouldShowSelectOutput ? Visibility::Visible : Visibility::Collapsed);
(_core.HasSelection() ? SelectionContextMenu() :
ContextMenu())
.ShowAt(*this, myOption);
@@ -3378,4 +3387,19 @@ namespace winrt::Microsoft::Terminal::Control::implementation
SearchMatch(false);
}
void TermControl::_SelectCommandHandler(const IInspectable& /*sender*/,
const IInspectable& /*args*/)
{
ContextMenu().Hide();
SelectionContextMenu().Hide();
_core.ContextMenuSelectCommand();
}
void TermControl::_SelectOutputHandler(const IInspectable& /*sender*/,
const IInspectable& /*args*/)
{
ContextMenu().Hide();
SelectionContextMenu().Hide();
_core.ContextMenuSelectOutput();
}
}

View File

@@ -232,10 +232,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
bool _isBackgroundLight{ false };
bool _detached{ false };
winrt::Windows::Foundation::Collections::IObservableVector<winrt::Windows::UI::Xaml::Controls::ICommandBarElement> _originalPrimaryElements{ nullptr };
winrt::Windows::Foundation::Collections::IObservableVector<winrt::Windows::UI::Xaml::Controls::ICommandBarElement> _originalSecondaryElements{ nullptr };
winrt::Windows::Foundation::Collections::IObservableVector<winrt::Windows::UI::Xaml::Controls::ICommandBarElement> _originalSelectedPrimaryElements{ nullptr };
winrt::Windows::Foundation::Collections::IObservableVector<winrt::Windows::UI::Xaml::Controls::ICommandBarElement> _originalSelectedSecondaryElements{ nullptr };
Windows::Foundation::Collections::IObservableVector<Windows::UI::Xaml::Controls::ICommandBarElement> _originalPrimaryElements{ nullptr };
Windows::Foundation::Collections::IObservableVector<Windows::UI::Xaml::Controls::ICommandBarElement> _originalSecondaryElements{ nullptr };
Windows::Foundation::Collections::IObservableVector<Windows::UI::Xaml::Controls::ICommandBarElement> _originalSelectedPrimaryElements{ nullptr };
Windows::Foundation::Collections::IObservableVector<Windows::UI::Xaml::Controls::ICommandBarElement> _originalSelectedSecondaryElements{ nullptr };
inline bool _IsClosing() const noexcept
{
@@ -347,6 +347,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void _CopyCommandHandler(const IInspectable& sender, const IInspectable& args);
void _SearchCommandHandler(const IInspectable& sender, const IInspectable& args);
void _SelectCommandHandler(const IInspectable& sender, const IInspectable& args);
void _SelectOutputHandler(const IInspectable& sender, const IInspectable& args);
struct Revokers
{
Control::ControlCore::ScrollPositionChanged_revoker coreScrollPositionChanged;

View File

@@ -37,6 +37,21 @@
x:Uid="PasteCommandButton"
Click="_PasteCommandHandler"
Icon="Paste" />
<mux:CommandBarFlyout.SecondaryCommands>
<AppBarButton x:Name="SelectCommandButton"
x:Uid="SelectCommandButton"
Click="_SelectCommandHandler">
<AppBarButton.Icon>
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="11"
Glyph="&#xE756;" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton x:Name="SelectOutputButton"
x:Uid="SelectOutputButton"
Click="_SelectOutputHandler"
Icon="AlignLeft" />
</mux:CommandBarFlyout.SecondaryCommands>
</mux:CommandBarFlyout>
<mux:CommandBarFlyout x:Name="SelectionContextMenu">
@@ -53,6 +68,19 @@
x:Uid="SearchCommandButton"
Click="_SearchCommandHandler"
Icon="Find" />
<AppBarButton x:Name="SelectCommandWithSelectionButton"
x:Uid="SelectCommandWithSelectionButton"
Click="_SelectCommandHandler">
<AppBarButton.Icon>
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="11"
Glyph="&#xE756;" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton x:Name="SelectOutputWithSelectionButton"
x:Uid="SelectOutputWithSelectionButton"
Click="_SelectOutputHandler"
Icon="AlignLeft" />
</mux:CommandBarFlyout.SecondaryCommands>
</mux:CommandBarFlyout>

View File

@@ -798,6 +798,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
{
BASIC_FACTORY(ActionEventArgs);
BASIC_FACTORY(CopyTextArgs);
BASIC_FACTORY(SwitchToTabArgs);
BASIC_FACTORY(NewTerminalArgs);
BASIC_FACTORY(NewTabArgs);

View File

@@ -154,6 +154,7 @@ namespace Microsoft.Terminal.Settings.Model
[default_interface] runtimeclass CopyTextArgs : IActionArgs
{
CopyTextArgs();
Boolean SingleLine { get; };
Windows.Foundation.IReference<Microsoft.Terminal.Control.CopyFormat> CopyFormatting { get; };
};

View File

@@ -8,7 +8,7 @@ namespace til
// Method Description:
// - Base case provided to handle the last argument to coalesce_value<T...>()
template<typename T>
T coalesce_value(const T& base)
T coalesce_value(const T& base) noexcept
{
return base;
}

View File

@@ -589,5 +589,10 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
{
return outputEnd.has_value() && *outputEnd != *commandEnd;
}
std::pair<til::point, til::point> GetExtent() const
{
til::point realEnd{ til::coalesce_value(outputEnd, commandEnd, end) };
return std::make_pair(til::point{ start }, realEnd);
}
};
}