diff --git a/src/cascadia/TerminalApp/ColorPickupFlyout.cpp b/src/cascadia/TerminalApp/ColorPickupFlyout.cpp index e582ca7fe6f..f82d86feecc 100644 --- a/src/cascadia/TerminalApp/ColorPickupFlyout.cpp +++ b/src/cascadia/TerminalApp/ColorPickupFlyout.cpp @@ -1,9 +1,6 @@ #include "pch.h" #include "ColorPickupFlyout.h" #include "ColorPickupFlyout.g.cpp" -#include "winrt/Windows.UI.Xaml.Media.h" -#include "winrt/Windows.UI.Xaml.Shapes.h" -#include "winrt/Windows.UI.Xaml.Interop.h" #include namespace winrt::TerminalApp::implementation diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index fa5194db7ab..2d7a385fbb2 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -205,6 +205,15 @@ Close Tab + + Close Pane + + + Split Tab + + + Split Pane + Color... @@ -708,9 +717,6 @@ Open a new tab in given starting directory - - Split Tab - Open a new window with given starting directory @@ -795,4 +801,4 @@ Empty... - \ No newline at end of file + diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 6cfbc4bdf0b..f281ebadbd4 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1616,6 +1616,9 @@ namespace winrt::TerminalApp::implementation }); term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); + + term.ContextMenu().Opening({ this, &TerminalPage::_ContextMenuOpened }); + term.SelectionContextMenu().Opening({ this, &TerminalPage::_SelectionMenuOpened }); } // Method Description: @@ -4305,6 +4308,78 @@ namespace winrt::TerminalApp::implementation _updateThemeColors(); } + void TerminalPage::_ContextMenuOpened(const IInspectable& sender, + const IInspectable& /*args*/) + { + _PopulateContextMenu(sender, false /*withSelection*/); + } + void TerminalPage::_SelectionMenuOpened(const IInspectable& sender, + const IInspectable& /*args*/) + { + _PopulateContextMenu(sender, true /*withSelection*/); + } + + void TerminalPage::_PopulateContextMenu(const IInspectable& sender, + const bool /*withSelection*/) + { + // withSelection can be used to add actions that only appear if there's + // selected text, like "search the web". In this initial draft, it's not + // actually augmented by the TerminalPage, so it's left commented out. + + const auto& menu{ sender.try_as() }; + if (!menu) + { + return; + } + + // Helper lambda for dispatching an ActionAndArgs onto the + // ShortcutActionDispatch. Used below to wire up each menu entry to the + // respective action. + + auto weak = get_weak(); + auto makeCallback = [weak](const ActionAndArgs& actionAndArgs) { + return [weak, actionAndArgs](auto&&, auto&&) { + if (auto page{ weak.get() }) + { + page->_actionDispatch->DoAction(actionAndArgs); + } + }; + }; + + auto makeItem = [&menu, &makeCallback](const winrt::hstring& label, + const winrt::hstring& icon, + const auto& action) { + AppBarButton button{}; + + if (!icon.empty()) + { + auto iconElement = IconPathConverter::IconWUX(icon); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + button.Icon(iconElement); + } + + button.Label(label); + button.Click(makeCallback(action)); + menu.SecondaryCommands().Append(button); + }; + + // Wire up each item to the action that should be performed. By actually + // connecting these to actions, we ensure the implementation is + // consistent. This also leaves room for customizing this menu with + // actions in the future. + + makeItem(RS_(L"SplitPaneText"), L"\xF246", ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate } }); + makeItem(RS_(L"DuplicateTabText"), L"\xF5ED", ActionAndArgs{ ShortcutAction::DuplicateTab, nullptr }); + + // Only wire up "Close Pane" if there's multiple panes. + if (_GetFocusedTabImpl()->GetLeafPaneCount() > 1) + { + makeItem(RS_(L"PaneClose"), L"\xE89F", ActionAndArgs{ ShortcutAction::ClosePane, nullptr }); + } + + makeItem(RS_(L"TabClose"), L"\xE711", ActionAndArgs{ ShortcutAction::CloseTab, CloseTabArgs{ _GetFocusedTabIndex().value() } }); + } + // Handler for our WindowProperties's PropertyChanged event. We'll use this // to pop the "Identify Window" toast when the user renames our window. winrt::fire_and_forget TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, @@ -4328,4 +4403,5 @@ namespace winrt::TerminalApp::implementation } } } + } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index a99854374ca..f141d46025b 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -457,6 +457,10 @@ namespace winrt::TerminalApp::implementation winrt::fire_and_forget _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); winrt::fire_and_forget _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); + void _ContextMenuOpened(const IInspectable& sender, const IInspectable& args); + void _SelectionMenuOpened(const IInspectable& sender, const IInspectable& args); + void _PopulateContextMenu(const IInspectable& sender, const bool withSelection); + #pragma region ActionHandlers // These are all defined in AppActionHandlers.cpp #define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action); diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index 7eaf4275535..b4b6915c7db 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -205,7 +205,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // GH#9396: we prioritize hyper-link over VT mouse events auto hyperlink = _core->GetHyperlink(terminalPosition.to_core_point()); if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown) && - ctrlEnabled && !hyperlink.empty()) + ctrlEnabled && + !hyperlink.empty()) { const auto clickCount = _numberOfClicks(pixelPosition, timestamp); // Handle hyper-link only on the first click to prevent multiple activations @@ -255,14 +256,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation } else if (WI_IsFlagSet(buttonState, MouseButtonState::IsRightButtonDown)) { - // Try to copy the text and clear the selection - const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, nullptr); - _core->ClearSelection(); - if (_core->CopyOnSelect() || !successfulCopy) + if (_core->Settings().RightClickContextMenu()) { - // CopyOnSelect: right click always pastes! - // Otherwise: no selection --> paste - RequestPasteTextFromClipboard(); + auto contextArgs = winrt::make(til::point{ pixelPosition }.to_winrt_point()); + _ContextMenuRequestedHandlers(*this, contextArgs); + } + else + { + // Try to copy the text and clear the selection + const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, nullptr); + _core->ClearSelection(); + if (_core->CopyOnSelect() || !successfulCopy) + { + // CopyOnSelect: right click always pastes! + // Otherwise: no selection --> paste + RequestPasteTextFromClipboard(); + } } } } diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index 4cc4df10bdd..15fee5574c5 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -88,6 +88,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs); TYPED_EVENT(ScrollPositionChanged, IInspectable, Control::ScrollPositionChangedArgs); + TYPED_EVENT(ContextMenuRequested, IInspectable, Control::ContextMenuRequestedEventArgs); private: // NOTE: _uiaEngine must be ordered before _core. diff --git a/src/cascadia/TerminalControl/ControlInteractivity.idl b/src/cascadia/TerminalControl/ControlInteractivity.idl index aada01ee0ad..6906999aa38 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.idl +++ b/src/cascadia/TerminalControl/ControlInteractivity.idl @@ -65,6 +65,9 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler ScrollPositionChanged; event Windows.Foundation.TypedEventHandler PasteFromClipboard; + // Used to communicate to the TermControl, but not necessarily higher up in the stack + event Windows.Foundation.TypedEventHandler ContextMenuRequested; + }; } diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index 9f9b41ac1f7..c4089ebd76c 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -5,6 +5,7 @@ #include "EventArgs.h" #include "TitleChangedEventArgs.g.cpp" #include "CopyToClipboardEventArgs.g.cpp" +#include "ContextMenuRequestedEventArgs.g.cpp" #include "PasteFromClipboardEventArgs.g.cpp" #include "OpenHyperlinkEventArgs.g.cpp" #include "NoticeEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index a542e391f63..2551843df20 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -5,6 +5,7 @@ #include "TitleChangedEventArgs.g.h" #include "CopyToClipboardEventArgs.g.h" +#include "ContextMenuRequestedEventArgs.g.h" #include "PasteFromClipboardEventArgs.g.h" #include "OpenHyperlinkEventArgs.g.h" #include "NoticeEventArgs.g.h" @@ -53,6 +54,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::IReference _formats; }; + struct ContextMenuRequestedEventArgs : public ContextMenuRequestedEventArgsT + { + public: + ContextMenuRequestedEventArgs(winrt::Windows::Foundation::Point pos) : + _Position(pos) {} + + WINRT_PROPERTY(winrt::Windows::Foundation::Point, Position); + }; + struct PasteFromClipboardEventArgs : public PasteFromClipboardEventArgsT { public: diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 941384c5b05..bc67bd13624 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -22,6 +22,11 @@ namespace Microsoft.Terminal.Control Windows.Foundation.IReference Formats { get; }; } + runtimeclass ContextMenuRequestedEventArgs + { + Windows.Foundation.Point Position { get; }; + } + runtimeclass TitleChangedEventArgs { String Title; diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl index a47af4cc262..1e2f7d71f24 100644 --- a/src/cascadia/TerminalControl/IControlSettings.idl +++ b/src/cascadia/TerminalControl/IControlSettings.idl @@ -61,5 +61,6 @@ namespace Microsoft.Terminal.Control Boolean SoftwareRendering { get; }; Boolean ShowMarks { get; }; Boolean UseBackgroundImageForWindow { get; }; + Boolean RightClickContextMenu { get; }; }; } diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index cff7cb1c9bd..aa2473890f2 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -208,4 +208,36 @@ Please either install the missing font or choose another one. No results found Announced to a screen reader when the user searches for some text and there are no matches for that text in the terminal. - \ No newline at end of file + + Paste + The label of a button for pasting the contents of the clipboard. + + + Paste + The tooltip for a paste button + + + Copy + The label of a button for copying the selected text to the clipboard. + + + Copy + The tooltip for a copy button + + + Paste + The label of a button for pasting the contents of the clipboard. + + + Paste + The tooltip for a paste button + + + Find... + The label of a button for searching for the selected text + + + Find + The tooltip for a button for searching for the selected text + + diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index 5f7dee504d7..4a680edcb68 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -14,8 +14,6 @@ Author(s): --*/ #pragma once -#include "winrt/Windows.UI.Xaml.h" -#include "winrt/Windows.UI.Xaml.Controls.h" #include "SearchBoxControl.g.h" diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index c9ef1550f18..40081c30b62 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -88,6 +88,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation _interactivity.OpenHyperlink({ this, &TermControl::_HyperlinkHandler }); _interactivity.ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); + _interactivity.ContextMenuRequested({ this, &TermControl::_contextMenuHandler }); + // Initialize the terminal only once the swapchainpanel is loaded - that // way, we'll be able to query the real pixel size it got on layout _layoutUpdatedRevoker = SwapChainPanel().LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { @@ -135,6 +137,59 @@ namespace winrt::Microsoft::Terminal::Control::implementation _autoScrollTimer.Tick({ this, &TermControl::_UpdateAutoScroll }); _ApplyUISettings(); + + _originalPrimaryElements = winrt::single_threaded_observable_vector(); + _originalSecondaryElements = winrt::single_threaded_observable_vector(); + _originalSelectedPrimaryElements = winrt::single_threaded_observable_vector(); + _originalSelectedSecondaryElements = winrt::single_threaded_observable_vector(); + for (const auto& e : ContextMenu().PrimaryCommands()) + { + _originalPrimaryElements.Append(e); + } + for (const auto& e : ContextMenu().SecondaryCommands()) + { + _originalSecondaryElements.Append(e); + } + for (const auto& e : SelectionContextMenu().PrimaryCommands()) + { + _originalSelectedPrimaryElements.Append(e); + } + for (const auto& e : SelectionContextMenu().SecondaryCommands()) + { + _originalSelectedSecondaryElements.Append(e); + } + ContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) { + if (auto control{ weakThis.get() }; !control->_IsClosing()) + { + const auto& menu{ control->ContextMenu() }; + menu.PrimaryCommands().Clear(); + menu.SecondaryCommands().Clear(); + for (const auto& e : control->_originalPrimaryElements) + { + menu.PrimaryCommands().Append(e); + } + for (const auto& e : control->_originalSecondaryElements) + { + menu.SecondaryCommands().Append(e); + } + } + }); + SelectionContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) { + if (auto control{ weakThis.get() }; !control->_IsClosing()) + { + const auto& menu{ control->SelectionContextMenu() }; + menu.PrimaryCommands().Clear(); + menu.SecondaryCommands().Clear(); + for (const auto& e : control->_originalSelectedPrimaryElements) + { + menu.PrimaryCommands().Append(e); + } + for (const auto& e : control->_originalSelectedSecondaryElements) + { + menu.SecondaryCommands().Append(e); + } + } + }); } void TermControl::_throttledUpdateScrollbar(const ScrollBarUpdate& update) @@ -292,7 +347,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - Given Settings having been updated, applies the settings to the current terminal. // Return Value: // - - winrt::fire_and_forget TermControl::UpdateControlSettings(IControlSettings settings, IControlAppearance unfocusedAppearance) + winrt::fire_and_forget TermControl::UpdateControlSettings(IControlSettings settings, + IControlAppearance unfocusedAppearance) { auto weakThis{ get_weak() }; @@ -3174,4 +3230,52 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _core.ColorSelection(fg, bg, matchMode); } + + void TermControl::_contextMenuHandler(IInspectable /*sender*/, + Control::ContextMenuRequestedEventArgs args) + { + Controls::Primitives::FlyoutShowOptions myOption{}; + myOption.ShowMode(Controls::Primitives::FlyoutShowMode::Standard); + myOption.Placement(Controls::Primitives::FlyoutPlacementMode::TopEdgeAlignedLeft); + + // Position the menu where the pointer is. This was the best way I found how. + const til::point absolutePointerPos{ til::math::rounding, CoreWindow::GetForCurrentThread().PointerPosition() }; + const til::point absoluteWindowOrigin{ til::math::rounding, + CoreWindow::GetForCurrentThread().Bounds().X, + CoreWindow::GetForCurrentThread().Bounds().Y }; + // Get the offset (margin + tabs, etc..) of the control within the window + const til::point controlOrigin{ til::math::flooring, + this->TransformToVisual(nullptr).TransformPoint(Windows::Foundation::Point(0, 0)) }; + + const auto pos = (absolutePointerPos - absoluteWindowOrigin - controlOrigin).to_winrt_point(); + myOption.Position(pos); + + (_core.HasSelection() ? SelectionContextMenu() : + ContextMenu()) + .ShowAt(*this, myOption); + } + + void TermControl::_PasteCommandHandler(const IInspectable& /*sender*/, + const IInspectable& /*args*/) + { + _interactivity.RequestPasteTextFromClipboard(); + ContextMenu().Hide(); + SelectionContextMenu().Hide(); + } + void TermControl::_CopyCommandHandler(const IInspectable& /*sender*/, + const IInspectable& /*args*/) + { + // formats = nullptr -> copy all formats + _interactivity.CopySelectionToClipboard(false, nullptr); + ContextMenu().Hide(); + SelectionContextMenu().Hide(); + } + void TermControl::_SearchCommandHandler(const IInspectable& /*sender*/, + const IInspectable& /*args*/) + { + ContextMenu().Hide(); + SelectionContextMenu().Hide(); + SearchMatch(false); + } + } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 590780e7524..e8f0bb8734d 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -160,6 +160,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TYPED_EVENT(FocusFollowMouseRequested, IInspectable, IInspectable); TYPED_EVENT(Initialized, Control::TermControl, Windows::UI::Xaml::RoutedEventArgs); TYPED_EVENT(WarningBell, IInspectable, IInspectable); + // clang-format on WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, BackgroundBrush, _PropertyChangedHandlers, nullptr); @@ -219,6 +220,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool _isBackgroundLight{ false }; + winrt::Windows::Foundation::Collections::IObservableVector _originalPrimaryElements{ nullptr }; + winrt::Windows::Foundation::Collections::IObservableVector _originalSecondaryElements{ nullptr }; + winrt::Windows::Foundation::Collections::IObservableVector _originalSelectedPrimaryElements{ nullptr }; + winrt::Windows::Foundation::Collections::IObservableVector _originalSelectedSecondaryElements{ nullptr }; + inline bool _IsClosing() const noexcept { #ifndef NDEBUG @@ -315,6 +321,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::point _toPosInDips(const Core::Point terminalCellPos); void _throttledUpdateScrollbar(const ScrollBarUpdate& update); + + void _contextMenuHandler(IInspectable sender, Control::ContextMenuRequestedEventArgs args); + + void _PasteCommandHandler(const IInspectable& sender, const IInspectable& args); + void _CopyCommandHandler(const IInspectable& sender, const IInspectable& args); + void _SearchCommandHandler(const IInspectable& sender, const IInspectable& args); }; } diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 044bfb0a6b1..3c9f794f42c 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -45,6 +45,9 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler ReadOnlyChanged; event Windows.Foundation.TypedEventHandler FocusFollowMouseRequested; + Microsoft.UI.Xaml.Controls.CommandBarFlyout ContextMenu { get; }; + Microsoft.UI.Xaml.Controls.CommandBarFlyout SelectionContextMenu { get; }; + event Windows.Foundation.TypedEventHandler Initialized; // This is an event handler forwarder for the underlying connection. // We expose this and ConnectionState here so that it might eventually be data bound. diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index 0f5b6b86b3e..58b5c8b08d0 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -10,6 +10,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:Microsoft.Terminal.Control" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:mux="using:Microsoft.UI.Xaml.Controls" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" d:DesignHeight="768" @@ -31,6 +32,30 @@ mc:Ignorable="d"> + + + + + + + + + + + +