Implement custom text context menus to fix crashes (#18854)

This works around a bug in WinUI where it creates a single context
menu/flyout for text elements per thread, not per `XamlRoot`, similar to
many other areas. Since the `XamlRoot` cannot change after creation,
this means that once you've opened the flyout, you're locked into that
window (= XAML root) forever. You can't open the flyout in another
window and once you've closed that window, you can't open it anywhere at
all.

Closes #18599

* Flies out right click in the
  * About dialog 
  * Search dialog 
  * Word delimiters setting 
  * Launch size setting 
* Across two windows 

(cherry picked from commit 076746a7a62197111981ca706eac9ff948286133)
Service-Card-Id: PVTI_lADOAF3p4s4Axadtzgd5l-U
Service-Version: 1.23
This commit is contained in:
Leonard Hecker 2025-05-14 21:47:23 +02:00 committed by Dustin L. Howett
parent 59ff525128
commit eae5fbd83d
28 changed files with 520 additions and 34 deletions

View File

@ -128,6 +128,7 @@ Blt
blu
BLUESCROLL
bmi
bodgy
BODGY
BOLDFONT
Borland
@ -371,8 +372,8 @@ Dcd
DColor
dcommon
DComposition
dde
DDDCCC
dde
DDESHARE
DDevice
DEADCHAR

View File

@ -8,6 +8,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
x:Uid="AboutDialog"
DefaultButton="Close"
@ -17,6 +18,9 @@
<StackPanel Orientation="Vertical">
<TextBlock IsTextSelectionEnabled="True">
<TextBlock.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBlock.ContextFlyout>
<Run AutomationProperties.HeadingLevel="1"
Text="{x:Bind ApplicationDisplayName}" /> <LineBreak />
<Run x:Uid="AboutDialog_VersionLabel" />
@ -39,7 +43,7 @@
VerticalAlignment="Center"
Orientation="Vertical"
Visibility="{x:Bind UpdatesAvailable, Mode=OneWay}">
<TextBlock IsTextSelectionEnabled="False">
<TextBlock>
<Run x:Uid="AboutDialog_UpdateAvailableLabel" />
</TextBlock>
<!-- <Button x:Uid="AboutDialog_InstallUpdateButton"
@ -59,4 +63,3 @@
Click="_ThirdPartyNoticesOnClick" />
</StackPanel>
</ContentDialog>

View File

@ -284,7 +284,11 @@
IsSpellCheckEnabled="False"
PlaceholderText="{x:Bind SearchBoxPlaceholderText, Mode=OneWay}"
Text=""
TextChanged="_filterTextChanged" />
TextChanged="_filterTextChanged">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
</TextBox>
<TextBlock x:Name="_prefixCharacter"
Grid.Row="0"

View File

@ -78,6 +78,7 @@ namespace winrt::TerminalApp::implementation
void MarkdownPaneContent::_loadText()
{
auto block = WUX::Controls::TextBlock();
block.ContextFlyout(winrt::Microsoft::Terminal::UI::TextMenuFlyout{});
block.IsTextSelectionEnabled(true);
block.FontFamily(WUX::Media::FontFamily{ L"Cascadia Code" });
block.Text(FileContents());

View File

@ -8,6 +8,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d">
@ -51,7 +52,11 @@
Grid.Column="0"
Margin="4"
PlaceholderText="Enter a path to a markdown file..."
Text="Z:\dev\simple-test.md" />
Text="Z:\dev\simple-test.md">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
</TextBox>
<StackPanel Grid.Column="1"
Orientation="Horizontal">
<Button Margin="4"
@ -105,7 +110,11 @@
FontFamily="Cascadia Code"
IsSpellCheckEnabled="False"
Text="{x:Bind FileContents, Mode=TwoWay}"
Visibility="{x:Bind Editing}" />
Visibility="{x:Bind Editing}">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
</TextBox>
<ScrollViewer x:Name="_scrollViewer"
Grid.Column="1"

View File

@ -20,6 +20,7 @@ namespace winrt::TerminalApp::implementation
_root.Background(bg.try_as<Media::Brush>());
_box = winrt::Windows::UI::Xaml::Controls::TextBox{};
_box.ContextFlyout(winrt::Microsoft::Terminal::UI::TextMenuFlyout{});
_box.Margin({ 10, 10, 10, 10 });
_box.AcceptsReturn(true);
_box.TextWrapping(TextWrapping::Wrap);

View File

@ -182,7 +182,6 @@
Grid.Column="1"
Margin="12,0,12,6"
FontFamily="Cascadia Mono, Consolas"
IsTextSelectionEnabled="False"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind Input}"
Visibility="{Binding ElementName=rootItem, Path=IsSelected}" />
@ -263,7 +262,11 @@
Margin="8,0,8,8"
AllowFocusOnInteraction="True"
TextChanged="_filterTextChanged"
Visibility="{x:Bind HasSnippets, Mode=OneWay}" />
Visibility="{x:Bind HasSnippets, Mode=OneWay}">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
</TextBox>
<mux:TreeView x:Name="_treeView"
Grid.Row="2"

View File

@ -141,7 +141,11 @@
PlaceholderText="{x:Bind SearchBoxPlaceholderText, Mode=OneWay}"
Text=""
TextChanged="_filterTextChanged"
Visibility="Collapsed" />
Visibility="Collapsed">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
</TextBox>
<StackPanel Grid.Row="1"
Margin="8,0,8,8"
@ -224,7 +228,11 @@
FontSize="14"
FontWeight="Bold"
IsTextSelectionEnabled="True"
TextWrapping="WrapWholeWords" />
TextWrapping="WrapWholeWords">
<TextBlock.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBlock.ContextFlyout>
</TextBlock>
<ScrollViewer MaxHeight="64"
VerticalScrollBarVisibility="Visible"
VerticalScrollMode="Enabled"
@ -232,7 +240,11 @@
<TextBlock x:Name="_descriptionComment"
Margin="0,0,20,0"
IsTextSelectionEnabled="True"
TextWrapping="WrapWholeWords" />
TextWrapping="WrapWholeWords">
<TextBlock.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBlock.ContextFlyout>
</TextBlock>
</ScrollViewer>
</StackPanel>

View File

@ -9,6 +9,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
MinHeight="16"
mc:Ignorable="d">
@ -67,6 +68,10 @@
IsSpellCheckEnabled="False"
LostFocus="RenameBoxLostFocusHandler"
MaxLength="1024"
Visibility="Collapsed" />
Visibility="Collapsed">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
</TextBox>
</StackPanel>
</UserControl>

View File

@ -70,8 +70,7 @@
FontSize="12">
<ToolTipService.ToolTip>
<ToolTip Placement="Mouse">
<TextBlock IsTextSelectionEnabled="False"
TextWrapping="Wrap">
<TextBlock TextWrapping="Wrap">
<Run x:Uid="NewTabRun" /> <LineBreak />
<Run x:Uid="NewPaneRun"
FontStyle="Italic" /> <LineBreak />

View File

@ -137,6 +137,9 @@
DefaultButton="Primary">
<TextBlock IsTextSelectionEnabled="True"
TextWrapping="WrapWholeWords">
<TextBlock.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBlock.ContextFlyout>
<Run x:Name="NoticeMessage" />
</TextBlock>
</ContentDialog>
@ -148,6 +151,9 @@
DefaultButton="Primary">
<TextBlock IsTextSelectionEnabled="True"
TextWrapping="WrapWholeWords">
<TextBlock.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBlock.ContextFlyout>
<Run x:Name="CouldNotOpenUriReason" /> <LineBreak />
<Run x:Name="UnopenedUri"
FontFamily="Cascadia Mono" />
@ -191,7 +197,11 @@
<TextBox x:Name="WindowRenamerTextBox"
KeyDown="_WindowRenamerKeyDown"
KeyUp="_WindowRenamerKeyUp"
Text="{x:Bind WindowProperties.WindowName, Mode=OneWay}" />
Text="{x:Bind WindowProperties.WindowName, Mode=OneWay}">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
</TextBox>
</mux:TeachingTip.Content>
</mux:TeachingTip>

View File

@ -419,6 +419,7 @@ namespace winrt::TerminalApp::implementation
auto buttonText = RS_(L"Ok");
Controls::TextBlock warningsTextBlock;
warningsTextBlock.ContextFlyout(winrt::Microsoft::Terminal::UI::TextMenuFlyout{});
// Make sure you can copy-paste
warningsTextBlock.IsTextSelectionEnabled(true);
// Make sure the lines of text wrap
@ -468,6 +469,7 @@ namespace winrt::TerminalApp::implementation
auto buttonText = RS_(L"Ok");
Controls::TextBlock warningsTextBlock;
warningsTextBlock.ContextFlyout(winrt::Microsoft::Terminal::UI::TextMenuFlyout{});
// Make sure you can copy-paste
warningsTextBlock.IsTextSelectionEnabled(true);
// Make sure the lines of text wrap

View File

@ -4,6 +4,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:mtu="using:Microsoft.Terminal.UI"
x:Name="Root"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
@ -197,7 +198,11 @@
VerticalAlignment="Center"
IsSpellCheckEnabled="False"
KeyDown="TextBoxKeyDown"
TextChanged="TextBoxTextChanged" />
TextChanged="TextBoxTextChanged">
<TextBox.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBox.ContextFlyout>
</TextBox>
<TextBlock x:Name="StatusBox"
x:Uid="SearchBox_StatusBox"

View File

@ -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:mtu="using:Microsoft.Terminal.UI"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
@ -1272,6 +1273,9 @@
Placement="Mouse">
<TextBlock IsTextSelectionEnabled="True"
TextWrapping="Wrap">
<TextBlock.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBlock.ContextFlyout>
<Run x:Name="HoveredUri" /> <LineBreak />
<Run x:Uid="HowToOpenRun"
FontStyle="Italic" />

View File

@ -132,6 +132,11 @@
<Style x:Key="TextBoxSettingStyle"
BasedOn="{StaticResource DefaultTextBoxStyle}"
TargetType="TextBox">
<Setter Property="ContextFlyout">
<Setter.Value>
<mtu:TextMenuFlyout />
</Setter.Value>
</Setter>
<Setter Property="MinWidth" Value="{StaticResource StandardBoxMinWidth}" />
<Setter Property="MaxWidth" Value="400" />
<Setter Property="HorizontalAlignment" Value="Left" />

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Cut" xml:space="preserve">
<value>Cut</value>
<comment>"Cut" as contained in a Cut/Copy/Paste menu</comment>
</data>
<data name="Copy" xml:space="preserve">
<value>Copy</value>
<comment>"Copy" as contained in a Cut/Copy/Paste menu</comment>
</data>
<data name="Paste" xml:space="preserve">
<value>Paste</value>
<comment>"Paste" as contained in a Cut/Copy/Paste menu</comment>
</data>
<data name="SelectAll" xml:space="preserve">
<value>Select All</value>
<comment>"Select All" as contained in a Cut/Copy/Paste menu</comment>
</data>
</root>

View File

@ -0,0 +1,204 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "TextMenuFlyout.h"
#include <LibraryResources.h>
#include "TextMenuFlyout.g.cpp"
using namespace ::winrt::Windows::UI::Xaml;
using namespace ::winrt::Windows::UI::Xaml::Controls;
using namespace ::winrt::Windows::ApplicationModel::Resources::Core;
using namespace ::winrt::Microsoft::UI::Xaml::Controls;
using namespace ::winrt::Windows::System;
using namespace ::winrt::Windows::UI::Xaml::Input;
using MenuFlyoutItemClick = void (*)(IInspectable const&, RoutedEventArgs const&);
constexpr auto NullSymbol = static_cast<Symbol>(0);
namespace winrt::Microsoft::Terminal::UI::implementation
{
#pragma warning(suppress : 26455) // Default constructor should not throw. Declare it 'noexcept' (f.6).
TextMenuFlyout::TextMenuFlyout()
{
// Most of the initialization is delayed until the first call to MenuFlyout_Opening.
Opening({ this, &TextMenuFlyout::MenuFlyout_Opening });
}
void TextMenuFlyout::MenuFlyout_Opening(IInspectable const&, IInspectable const&)
{
auto target = Target();
if (!target)
{
return;
}
hstring selection;
bool writable = false;
// > Common group of selectable controls with common actions
// > The I in MIDL stands for...
// No common interface.
if (const auto box = target.try_as<NumberBox>())
{
target = box.GetTemplateChild(L"InputBox").as<TextBox>();
}
if (const auto control = target.try_as<TextBlock>())
{
selection = control.SelectedText();
}
else if (const auto control = target.try_as<TextBox>())
{
selection = control.SelectedText();
writable = true;
}
else if (const auto control = target.try_as<RichTextBlock>())
{
selection = control.SelectedText();
}
if (!_copy)
{
til::small_vector<MenuFlyoutItemBase, 4> items;
if (writable)
{
_cut = items.emplace_back(_createMenuItem(Symbol::Cut, RS_(L"Cut"), { this, &TextMenuFlyout::Cut_Click }, VirtualKeyModifiers::Control, VirtualKey::X));
}
_copy = items.emplace_back(_createMenuItem(Symbol::Copy, RS_(L"Copy"), { this, &TextMenuFlyout::Copy_Click }, VirtualKeyModifiers::Control, VirtualKey::C));
if (writable)
{
items.emplace_back(_createMenuItem(Symbol::Paste, RS_(L"Paste"), { this, &TextMenuFlyout::Paste_Click }, VirtualKeyModifiers::Control, VirtualKey::V));
}
items.emplace_back(_createMenuItem(NullSymbol, RS_(L"SelectAll"), { this, &TextMenuFlyout::SelectAll_Click }, VirtualKeyModifiers::Control, VirtualKey::A));
Items().ReplaceAll({ items.data(), gsl::narrow_cast<uint32_t>(items.size()) });
}
const auto visibilityOfItemsRequiringSelection = selection.empty() ? Visibility::Collapsed : Visibility::Visible;
if (_cut)
{
_cut.Visibility(visibilityOfItemsRequiringSelection);
}
_copy.Visibility(visibilityOfItemsRequiringSelection);
}
void TextMenuFlyout::Cut_Click(IInspectable const&, RoutedEventArgs const&)
{
// NOTE: When the flyout closes, WinUI doesn't disconnect the accelerator keys.
// Since that means we'll get Ctrl+X/C/V callbacks forever, just ignore them.
// The TextBox will still handle those events...
auto target = Target();
if (!target)
{
return;
}
if (const auto box = target.try_as<NumberBox>())
{
target = box.GetTemplateChild(L"InputBox").as<TextBox>();
}
if (const auto control = target.try_as<TextBox>())
{
control.CutSelectionToClipboard();
}
}
void TextMenuFlyout::Copy_Click(IInspectable const&, RoutedEventArgs const&)
{
auto target = Target();
if (!target)
{
return;
}
if (const auto box = target.try_as<NumberBox>())
{
target = box.GetTemplateChild(L"InputBox").as<TextBox>();
}
if (const auto control = target.try_as<TextBlock>())
{
control.CopySelectionToClipboard();
}
else if (const auto control = target.try_as<TextBox>())
{
control.CopySelectionToClipboard();
}
else if (const auto control = target.try_as<RichTextBlock>())
{
control.CopySelectionToClipboard();
}
}
void TextMenuFlyout::Paste_Click(IInspectable const&, RoutedEventArgs const&)
{
auto target = Target();
if (!target)
{
return;
}
if (const auto box = target.try_as<NumberBox>())
{
target = box.GetTemplateChild(L"InputBox").as<TextBox>();
}
if (const auto control = target.try_as<TextBox>())
{
control.PasteFromClipboard();
}
}
void TextMenuFlyout::SelectAll_Click(IInspectable const&, RoutedEventArgs const&)
{
// BODGY:
// Once the flyout was open once, we'll get Ctrl+A events and the TextBox will
// ignore them. As such, we have to dig out the focused element as a fallback,
// because otherwise Ctrl+A will be permanently broken. Put differently,
// this is bodgy because WinUI 2.8 is buggy. There's no other solution here.
IInspectable target = Target();
if (!target)
{
target = FocusManager::GetFocusedElement(XamlRoot());
if (!target)
{
return;
}
}
if (const auto box = target.try_as<NumberBox>())
{
target = box.GetTemplateChild(L"InputBox").as<TextBox>();
}
if (const auto control = target.try_as<TextBlock>())
{
control.SelectAll();
}
else if (const auto control = target.try_as<TextBox>())
{
control.SelectAll();
}
else if (const auto control = target.try_as<RichTextBlock>())
{
control.SelectAll();
}
}
MenuFlyoutItemBase TextMenuFlyout::_createMenuItem(Symbol symbol, hstring text, RoutedEventHandler click, VirtualKeyModifiers modifiers, VirtualKey key)
{
KeyboardAccelerator accel;
accel.Modifiers(modifiers);
accel.Key(key);
MenuFlyoutItem item;
if (symbol != NullSymbol)
{
item.Icon(SymbolIcon{ std::move(symbol) });
}
item.Text(std::move(text));
item.Click(std::move(click));
item.KeyboardAccelerators().Append(std::move(accel));
return item;
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "TextMenuFlyout.g.h"
namespace winrt::Microsoft::Terminal::UI::implementation
{
// This custom flyout exists because WinUI 2 only supports 1 text block flyout
// *per thread* not per window. If you have >1 window per 1 thread, as we do,
// the focus will just be delegated to the window the flyout was first opened in.
// Once the first window is gone, WinUI will either do nothing or crash.
// See: GH#18599
struct TextMenuFlyout : TextMenuFlyoutT<TextMenuFlyout>
{
TextMenuFlyout();
void MenuFlyout_Opening(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::Foundation::IInspectable const& e);
void Cut_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
void Copy_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
void Paste_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
void SelectAll_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
private:
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItemBase _createMenuItem(
winrt::Windows::UI::Xaml::Controls::Symbol symbol,
winrt::hstring text,
winrt::Windows::UI::Xaml::RoutedEventHandler click,
winrt::Windows::System::VirtualKeyModifiers modifiers,
winrt::Windows::System::VirtualKey key);
// These are always present.
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItemBase _copy{ nullptr };
// These are only set for writable controls.
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItemBase _cut{ nullptr };
};
}
namespace winrt::Microsoft::Terminal::UI::factory_implementation
{
BASIC_FACTORY(TextMenuFlyout);
}

View File

@ -0,0 +1,7 @@
namespace Microsoft.Terminal.UI
{
[default_interface] runtimeclass TextMenuFlyout : Windows.UI.Xaml.Controls.MenuFlyout
{
TextMenuFlyout();
}
}

View File

@ -27,6 +27,9 @@
<ClInclude Include="ResourceString.h">
<DependentUpon>ResourceString.idl</DependentUpon>
</ClInclude>
<ClInclude Include="TextMenuFlyout.h">
<DependentUpon>TextMenuFlyout.idl</DependentUpon>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="init.cpp" />
@ -42,6 +45,9 @@
<ClCompile Include="ResourceString.cpp">
<DependentUpon>ResourceString.idl</DependentUpon>
</ClCompile>
<ClCompile Include="TextMenuFlyout.cpp">
<DependentUpon>TextMenuFlyout.idl</DependentUpon>
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
</ItemGroup>
<ItemGroup>
@ -49,6 +55,13 @@
<Midl Include="IconPathConverter.idl" />
<Midl Include="IDirectKeyListener.idl" />
<Midl Include="ResourceString.idl" />
<Midl Include="TextMenuFlyout.idl" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Resources\en-US\Resources.resw">
<SubType>Designer</SubType>
</PRIResource>
<OCResourceDirectory Include="Resources" />
</ItemGroup>
<!-- ========================= Project References ======================== -->
<ItemGroup>
@ -65,15 +78,12 @@
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<ItemDefinitionGroup>
<Link>
<AdditionalDependencies>%(AdditionalDependencies);user32.lib;shell32.lib</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.post.props" />
<!-- This -must- go after cppwinrt.build.post.props because that includes many VS-provided props including appcontainer.common.props, which stomps on what cppwinrt.targets did. -->
<Import Project="$(OpenConsoleDir)src\common.nugetversions.targets" />
</Project>

View File

@ -31,8 +31,18 @@
</ItemGroup>
<ItemGroup>
<Midl Include="ResourceString.idl" />
<Midl Include="Converters.idl" />
<Midl Include="IconPathConverter.idl" />
<Midl Include="IDirectKeyListener.idl" />
<Midl Include="TextMenuFlyout.idl" />
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Resources\en-US\Resources.resw">
<Filter>Resources\en-US</Filter>
</PRIResource>
</ItemGroup>
</Project>

View File

@ -3,6 +3,8 @@
#include "pch.h"
#include <LibraryResources.h>
#pragma warning(suppress : 26440) // Not interested in changing the specification of DllMain to make it noexcept given it's an interface to the OS.
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD reason, LPVOID /*reserved*/)
{
@ -11,11 +13,11 @@ BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD reason, LPVOID /*reserved*/)
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hInstDll);
break;
case DLL_PROCESS_DETACH:
break;
default:
break;
}
return TRUE;
}
UTILS_DEFINE_LIBRARY_RESOURCE_SCOPE(L"Microsoft.Terminal.UI/Resources")

View File

@ -25,16 +25,15 @@
#include <wil/cppwinrt.h>
#include <winrt/Windows.ApplicationModel.Resources.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.ApplicationModel.Resources.Core.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Graphics.Imaging.h>
#include <Windows.Graphics.Imaging.Interop.h>
#include <winrt/Windows.UI.Text.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Markup.h>
#include <winrt/Windows.UI.Xaml.Media.h>
#include <winrt/Windows.UI.Xaml.Input.h>
#include <winrt/Windows.UI.Xaml.Media.Imaging.h>
#include <winrt/Microsoft.UI.Xaml.Controls.h>

View File

@ -8,6 +8,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.Terminal.UI.Markdown"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
@ -201,7 +202,11 @@
VerticalAlignment="Stretch">
<TextBlock FontFamily="Cascadia Mono, Consolas"
IsTextSelectionEnabled="True"
Text="{x:Bind Commandlines}" />
Text="{x:Bind Commandlines}">
<TextBlock.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBlock.ContextFlyout>
</TextBlock>
</Grid>
</ScrollViewer>
</Border>

View File

@ -72,6 +72,7 @@ WUX::Controls::RichTextBlock MarkdownToXaml::Convert(std::string_view markdownTe
MarkdownToXaml::MarkdownToXaml(const winrt::hstring& baseUrl) :
_baseUri{ baseUrl }
{
_root.ContextFlyout(winrt::Microsoft::Terminal::UI::TextMenuFlyout{});
_root.IsTextSelectionEnabled(true);
_root.TextWrapping(WUX::TextWrapping::WrapWholeWords);
}
@ -155,6 +156,7 @@ void MarkdownToXaml::_EndParagraph() noexcept
WUX::Controls::TextBlock MarkdownToXaml::_makeDefaultTextBlock()
{
WUX::Controls::TextBlock b{};
b.ContextFlyout(winrt::Microsoft::Terminal::UI::TextMenuFlyout{});
b.IsTextSelectionEnabled(true);
b.TextWrapping(WUX::TextWrapping::WrapWholeWords);
return b;

View File

@ -7,7 +7,6 @@
<ConfigurationType>DynamicLibrary</ConfigurationType>
<SubSystem>Console</SubSystem>
<OpenConsoleUniversalApp>true</OpenConsoleUniversalApp>
<!-- C++/WinRT sets the depth to 1 if there is a XAML file in the project
Unfortunately for us, we need it to be 4. When the namespace merging
depth is 1, Microsoft.Terminal.UI.Markdown becomes "Microsoft",
@ -78,10 +77,11 @@
<Project>{CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}</Project>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
<ProjectReference Include="..\UIHelpers\UIHelpers.vcxproj">
<Project>{6515f03f-e56d-4db4-b23d-ac4fb80db36f}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.post.props" />
<!-- This -must- go after cppwinrt.build.post.props because that includes many VS-provided props including appcontainer.common.props, which stomps on what cppwinrt.targets did. -->
<Import Project="$(OpenConsoleDir)src\common.nugetversions.targets" />
</Project>

View File

@ -25,14 +25,20 @@
<ClCompile Include="pch.cpp">
<Filter>Module</Filter>
</ClCompile>
<ClCompile Include="MarkdownToXaml.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="MarkdownToXaml.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="Builder.idl" />
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
</Project>
<ItemGroup>
<Page Include="CodeBlock.xaml" />
</ItemGroup>
</Project>

View File

@ -25,21 +25,19 @@
#include <wil/cppwinrt.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.UI.Text.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Markup.h>
#include <winrt/Windows.UI.Xaml.Media.h>
#include <winrt/Windows.UI.Xaml.Media.Imaging.h>
#include <winrt/Windows.UI.Xaml.Documents.h>
#include <winrt/Windows.UI.Xaml.Input.h>
#include <winrt/Microsoft.UI.Xaml.Controls.h>
#include <winrt/Microsoft.UI.Xaml.Controls.Primitives.h>
#include <winrt/Microsoft.UI.Xaml.XamlTypeInfo.h>
#include <winrt/Microsoft.Terminal.UI.h>
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"