Browse Source

Merge branch 'master' into fix-grid-splitter-resize

Dariusz Komosiński 4 years ago
parent
commit
cfb67deb25
78 changed files with 2526 additions and 280 deletions
  1. 6 0
      samples/ControlCatalog/MainView.xaml
  2. 102 0
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
  3. 45 0
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs
  4. 264 0
      samples/ControlCatalog/Pages/FlyoutsPage.axaml
  5. 81 0
      samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
  6. 1 1
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  7. 6 0
      samples/ControlCatalog/Pages/TextBoxPage.xaml.cs
  8. 78 0
      samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs
  9. 96 0
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  10. 0 4
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  11. 12 0
      src/Android/Avalonia.Android/IInitEditorInfo.cs
  12. 1 1
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  13. 1 1
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  14. 30 16
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  15. 17 66
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
  16. 42 0
      src/Android/Avalonia.Android/SoftKeyboardListner.cs
  17. 24 4
      src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
  18. 2 2
      src/Avalonia.Base/Data/BindingValue.cs
  19. 45 9
      src/Avalonia.Base/EnumExtensions.cs
  20. 2 2
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  21. 1 1
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  22. 5 0
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  23. 51 0
      src/Avalonia.Controls/Button.cs
  24. 1 1
      src/Avalonia.Controls/ComboBox.cs
  25. 22 0
      src/Avalonia.Controls/Control.cs
  26. 8 8
      src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
  27. 50 0
      src/Avalonia.Controls/Flyouts/Flyout.cs
  28. 504 0
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  29. 77 0
      src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs
  30. 33 0
      src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
  31. 24 0
      src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs
  32. 75 0
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  33. 55 0
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  34. 5 5
      src/Avalonia.Controls/Grid.cs
  35. 4 4
      src/Avalonia.Controls/ListBox.cs
  36. 6 6
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  37. 6 6
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  38. 18 18
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  39. 6 6
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  40. 2 0
      src/Avalonia.Controls/ProgressBar.cs
  41. 2 2
      src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
  42. 43 7
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  43. 4 3
      src/Avalonia.Controls/TextBox.cs
  44. 5 5
      src/Avalonia.Controls/TreeView.cs
  45. 4 4
      src/Avalonia.Controls/Window.cs
  46. 8 8
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  47. 9 9
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  48. 4 4
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  49. 2 2
      src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
  50. 1 1
      src/Avalonia.Input/AccessKeyHandler.cs
  51. 4 4
      src/Avalonia.Input/KeyGesture.cs
  52. 5 5
      src/Avalonia.Input/PointerPoint.cs
  53. 3 3
      src/Avalonia.Interactivity/EventRoute.cs
  54. 2 2
      src/Avalonia.Interactivity/Interactive.cs
  55. 2 0
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  56. 32 0
      src/Avalonia.Themes.Default/FlyoutPresenter.xaml
  57. 29 0
      src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml
  58. 9 1
      src/Avalonia.Themes.Default/TextBox.xaml
  59. 5 0
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
  60. 5 0
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
  61. 2 0
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  62. 43 0
      src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml
  63. 35 0
      src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml
  64. 9 1
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  65. 2 2
      src/Avalonia.X11/X11Window.Ime.cs
  66. 9 9
      src/Avalonia.X11/X11Window.cs
  67. 5 5
      src/Avalonia.X11/XI2Manager.cs
  68. 1 1
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
  69. 4 4
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  70. 3 3
      src/Windows/Avalonia.Win32/DataObject.cs
  71. 12 12
      src/Windows/Avalonia.Win32/OleDropTarget.cs
  72. 7 7
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  73. 2 2
      src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
  74. 8 6
      src/Windows/Avalonia.Win32/WindowImpl.cs
  75. 27 6
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  76. 325 0
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
  77. 7 0
      tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs
  78. 39 1
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

+ 6 - 0
samples/ControlCatalog/MainView.xaml

@@ -21,6 +21,9 @@
       <TabItem Header="Carousel"><pages:CarouselPage/></TabItem>
       <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
       <TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem>
+      <TabItem Header="ContextFlyout">
+        <pages:ContextFlyoutPage/>
+      </TabItem>
       <TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
       <TabItem Header="Cursor"
                ScrollViewer.VerticalScrollBarVisibility="Disabled">
@@ -38,6 +41,9 @@
         <pages:CalendarDatePickerPage/></TabItem>
       <TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
       <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
+      <TabItem Header="Flyouts">
+        <pages:FlyoutsPage />
+      </TabItem>
       <TabItem Header="Image"
                ScrollViewer.VerticalScrollBarVisibility="Disabled"
                ScrollViewer.HorizontalScrollBarVisibility="Disabled">

+ 102 - 0
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml

@@ -0,0 +1,102 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="ControlCatalog.Pages.ContextFlyoutPage">
+    <UserControl.Styles>
+        <Style Selector="FlyoutPresenter.NoPadding">
+            <Setter Property="Padding" Value="0" />
+        </Style>
+    </UserControl.Styles>
+    
+    <StackPanel Orientation="Vertical" Spacing="4">
+        <TextBlock Classes="h1">Context Flyout</TextBlock>
+        <TextBlock Classes="h2">A right click Flyout that can be applied to any control.</TextBlock>
+
+        <StackPanel Orientation="Horizontal"
+              Margin="0,16,0,0"
+              HorizontalAlignment="Center"
+              Spacing="16">
+            <Border Background="{DynamicResource SystemAccentColor}"
+                    Margin="16"
+                    Padding="48,48,48,48">
+                <Border.ContextFlyout>
+                    <MenuFlyout>
+                        <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
+                        <MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
+                        <Separator/>
+                        <MenuItem Header="Menu with _Submenu">
+                            <MenuItem Header="Submenu _1"/>
+                            <MenuItem Header="Submenu _2"/>
+                        </MenuItem>
+                        <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
+                            <MenuItem.Icon>
+                                <Image Source="/Assets/github_icon.png"/>
+                            </MenuItem.Icon>
+                        </MenuItem>
+                        <MenuItem Header="Menu Item with _Checkbox">
+                            <MenuItem.Icon>
+                                <CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
+                            </MenuItem.Icon>
+                        </MenuItem>
+                    </MenuFlyout>
+                </Border.ContextFlyout>
+                <TextBlock Text="Defined in XAML"/>
+            </Border>
+            <Border Background="{DynamicResource SystemAccentColor}"
+                    Margin="16"
+                    Padding="48,48,48,48">
+                <Border.ContextMenu>
+                    <ContextMenu Items="{Binding MenuItems}">
+                        <ContextMenu.Styles>
+                            <Style Selector="MenuItem">
+                                <Setter Property="Header" Value="{Binding Header}"/>
+                                <Setter Property="Items" Value="{Binding Items}"/>
+                                <Setter Property="Command" Value="{Binding Command}"/>
+                                <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
+                            </Style>
+                        </ContextMenu.Styles>
+                    </ContextMenu>
+                </Border.ContextMenu>
+                <TextBlock Text="Dynamically Generated"/>
+            </Border>
+        </StackPanel>
+
+        <TextBlock Text="Custom ContextFlyout for TextBox" />
+
+        <TextBox Name="TextBox" Width="150" HorizontalAlignment="Center" ContextMenu="{x:Null}">
+            <TextBox.ContextFlyout>
+                <Flyout FlyoutPresenterClasses="NoPadding">
+                    <StackPanel Orientation="Horizontal">
+                        <StackPanel.Styles>
+                            <Style Selector="Button">
+                                <Setter Property="Background" Value="Transparent" />
+                                <Setter Property="Height" Value="40" />
+                                <Setter Property="Width" Value="40" />
+                                <Setter Property="VerticalContentAlignment" Value="Center" />
+                            </Style>
+                            <Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
+                                <Setter Property="Background" Value="Transparent" />
+                                <Setter Property="Opacity" Value="0.5" />
+                            </Style>
+                        </StackPanel.Styles>
+                        <Button Name="CutButton" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}">
+                            <PathIcon Width="14" Height="14" Data="M5.22774,2.08072 C5.43359778,1.94704 5.7011484,1.98419259 5.86368634,2.15675215 L5.91939,2.22774 L12.5191,12.3904 C12.956,12.1419 13.4614,12.0000019 14,12.0000019 C15.6569,12.0000019 17,13.3431 17,15.0000019 C17,16.6569 15.6569,18.0000019 14,18.0000019 C12.3431,18.0000019 11,16.6569 11,15.0000019 C11,14.3201402 11.226152,13.693011 11.6073785,13.1899092 L11.7401,13.0269 L10,10.3474 L8.25991,13.0269 C8.72078,13.5543 9,14.2446 9,15.0000019 C9,16.6569 7.65685,18.0000019 6,18.0000019 C4.34315,18.0000019 3,16.6569 3,15.0000019 C3,13.3431 4.34315,12.0000019 6,12.0000019 C6.46163143,12.0000019 6.89890041,12.1042536 7.28955831,12.2905296 L7.4809,12.3904 L9.40382,9.42936 L5.08072,2.77238 C4.93033,2.54079 4.99615,2.23112 5.22774,2.08072 Z M14,13 C12.8954,13 12,13.8954 12,15 C12,16.1046 12.8954,17 14,17 C15.1046,17 16,16.1046 16,15 C16,13.8954 15.1046,13 14,13 Z M6,13 C4.89543,13 4,13.8954 4,15 C4,16.1046 4.89543,17 6,17 C7.10457,17 8,16.1046 8,15 C8,13.8954 7.10457,13 6,13 Z M14.7723,2.08072 C15.0039,2.23112 15.0697,2.54079 14.9193,2.77238 L11.1924,8.51133 L10.5962,7.59329 L14.0806,2.22774 C14.231,1.99615 14.5407,1.93033 14.7723,2.08072 Z" />
+                        </Button>
+                        <Button Name="CopyButton" Content="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}">
+                            <PathIcon Width="14" Height="14" Data="M5.50280381,4.62704038 L5.5,6.75 L5.5,17.2542087 C5.5,19.0491342 6.95507456,20.5042087 8.75,20.5042087 L17.3662868,20.5044622 C17.057338,21.3782241 16.2239751,22.0042087 15.2444057,22.0042087 L8.75,22.0042087 C6.12664744,22.0042087 4,19.8775613 4,17.2542087 L4,6.75 C4,5.76928848 4.62744523,4.93512464 5.50280381,4.62704038 Z M17.75,2 C18.9926407,2 20,3.00735931 20,4.25 L20,17.25 C20,18.4926407 18.9926407,19.5 17.75,19.5 L8.75,19.5 C7.50735931,19.5 6.5,18.4926407 6.5,17.25 L6.5,4.25 C6.5,3.00735931 7.50735931,2 8.75,2 L17.75,2 Z M17.75,3.5 L8.75,3.5 C8.33578644,3.5 8,3.83578644 8,4.25 L8,17.25 C8,17.6642136 8.33578644,18 8.75,18 L17.75,18 C18.1642136,18 18.5,17.6642136 18.5,17.25 L18.5,4.25 C18.5,3.83578644 18.1642136,3.5 17.75,3.5 Z" />
+                        </Button>
+                        <Button Name="PasteButton" Content="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}">
+                            <PathIcon Width="14" Height="14" Data="M13.75,2 C14.940864,2 15.9156449,2.92516159 15.9948092,4.09595119 L16,4.25 L16,4.25 C16,4.16530567 15.9953205,4.0817043 15.9862059,3.99944035 L17.75,4 C18.9926407,4 20,5.00735931 20,6.25 L20,19.75 C20,20.9926407 18.9926407,22 17.75,22 L6.25,22 C5.00735931,22 4,20.9926407 4,19.75 L4,6.25 C4,5.00735931 5.00735931,4 6.25,4 L8.01379413,3.99944035 C8.00733496,4.05773764 8.00310309,4.11670658 8.00118552,4.17626017 L8,4.25 C8,3.00735931 9.00735931,2 10.25,2 L13.75,2 Z M13.75,6.5 L10.25,6.5 C9.45594921,6.5 8.75796956,6.08867052 8.357512,5.4674625 L8.37902077,5.50019943 L8.37902077,5.50019943 L6.25,5.5 C5.83578644,5.5 5.5,5.83578644 5.5,6.25 L5.5,19.75 C5.5,20.1642136 5.83578644,20.5 6.25,20.5 L17.75,20.5 C18.1642136,20.5 18.5,20.1642136 18.5,19.75 L18.5,6.25 C18.5,5.83578644 18.1642136,5.5 17.75,5.5 L15.6209792,5.50019943 L15.642488,5.4674625 C15.2420304,6.08867052 14.5440508,6.5 13.75,6.5 Z M13.75,3.5 L10.25,3.5 C9.83578644,3.5 9.5,3.83578644 9.5,4.25 C9.5,4.66421356 9.83578644,5 10.25,5 L13.75,5 C14.1642136,5 14.5,4.66421356 14.5,4.25 C14.5,3.83578644 14.1642136,3.5 13.75,3.5 Z" />
+                        </Button>
+                        <Button Name="ClearButton" Content="Clear" Command="{Binding $parent[TextBox].Clear}">
+                            <PathIcon Width="14" Height="14" Data="M3.52499419,3.71761187 L3.61611652,3.61611652 C4.0717282,3.16050485 4.79154862,3.13013074 5.28238813,3.52499419 L5.38388348,3.61611652 L14,12.233 L22.6161165,3.61611652 C23.1042719,3.12796116 23.8957281,3.12796116 24.3838835,3.61611652 C24.8720388,4.10427189 24.8720388,4.89572811 24.3838835,5.38388348 L15.767,14 L24.3838835,22.6161165 C24.8394952,23.0717282 24.8698693,23.7915486 24.4750058,24.2823881 L24.3838835,24.3838835 C23.9282718,24.8394952 23.2084514,24.8698693 22.7176119,24.4750058 L22.6161165,24.3838835 L14,15.767 L5.38388348,24.3838835 C4.89572811,24.8720388 4.10427189,24.8720388 3.61611652,24.3838835 C3.12796116,23.8957281 3.12796116,23.1042719 3.61611652,22.6161165 L12.233,14 L3.61611652,5.38388348 C3.16050485,4.9282718 3.13013074,4.20845138 3.52499419,3.71761187 L3.61611652,3.61611652 L3.52499419,3.71761187 Z" />
+                        </Button>
+                    </StackPanel>
+                </Flyout>
+            </TextBox.ContextFlyout>
+        </TextBox>
+    
+    </StackPanel>
+</UserControl>

+ 45 - 0
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs

@@ -0,0 +1,45 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+using Avalonia.Interactivity;
+namespace ControlCatalog.Pages
+{
+    public class ContextFlyoutPage : UserControl
+    {
+        private TextBox _textBox;
+
+        public ContextFlyoutPage()
+        {
+            InitializeComponent();
+
+            var vm = new ContextFlyoutPageViewModel();
+            vm.View = this;
+            DataContext = vm;
+
+            _textBox = this.FindControl<TextBox>("TextBox");
+
+            var cutButton = this.FindControl<Button>("CutButton");
+            cutButton.Click += CloseFlyout;
+
+            var copyButton = this.FindControl<Button>("CopyButton");
+            copyButton.Click += CloseFlyout;
+
+            var pasteButton = this.FindControl<Button>("PasteButton");
+            pasteButton.Click += CloseFlyout;
+
+            var clearButton = this.FindControl<Button>("ClearButton");
+            clearButton.Click += CloseFlyout;
+        }
+
+        private void CloseFlyout(object sender, RoutedEventArgs e)
+        {
+            _textBox.ContextFlyout.Hide();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 264 - 0
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@@ -0,0 +1,264 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700"
+             x:Class="ControlCatalog.Pages.FlyoutsPage">
+
+    <UserControl.Resources>
+        <MenuFlyout x:Key="SharedMenuFlyout">
+            <MenuItem Header="Item 1">
+                <MenuItem Header="Subitem 1" />
+                <MenuItem Header="Subitem 2" />
+                <MenuItem Header="Subitem 3" />
+            </MenuItem>
+            <MenuItem Header="Item 2" InputGesture="Ctrl+A" />
+            <MenuItem Header="Item 3" />
+        </MenuFlyout>
+        <Flyout Placement="Bottom" x:Key="BasicFlyout">
+            <Panel Width="100" Height="100">
+                <TextBlock Text="Flyout Content!" />
+            </Panel>
+        </Flyout>
+    </UserControl.Resources>
+
+    <ScrollViewer HorizontalScrollBarVisibility="Disabled">
+        <StackPanel Spacing="10">
+            <TextBlock FontSize="18" Text="Button with a Flyout" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <Button Content="Click Me!" Flyout="{StaticResource BasicFlyout}" />
+                </Border>
+                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                    <TextBlock Name="ButtonFlyoutXamlText" Padding="15" />
+                </Panel>
+            </StackPanel>
+
+            <TextBlock FontSize="18" Text="MenuFlyout" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <Button Content="Click Me!" Flyout="{StaticResource SharedMenuFlyout}" />
+                </Border>
+                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                    <TextBlock Name="MenuFlyoutXamlText" Padding="15" />
+                </Panel>
+            </StackPanel>
+
+            <TextBlock FontSize="18" Text="Attached Flyouts" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
+                           HorizontalAlignment="Left"
+                           Height="100"
+                           Name="AttachedFlyoutPanel">
+                        <FlyoutBase.AttachedFlyout>
+                            <Flyout>
+                                <Panel Height="100">
+                                    <TextBlock Text="Attached Flyout!"
+                                               VerticalAlignment="Center"
+                                               Margin="10"/>
+                                </Panel>
+                            </Flyout>
+                        </FlyoutBase.AttachedFlyout>
+
+                        <TextBlock Text="Double click panel to launch AttachedFlyout"
+                                   VerticalAlignment="Center"
+                                   Margin="10"/>
+
+                    </Panel>
+                </Border>
+                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                    <TextBlock Name="AttachedFlyoutXamlText" Padding="15" />
+                </Panel>
+            </StackPanel>
+
+
+            <TextBlock FontSize="18" Text="Sharing Flyouts" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <StackPanel Orientation="Horizontal" Spacing="30">
+                        <Button Content="Launch Flyout on this button" Flyout="{StaticResource SharedMenuFlyout}"/>
+                        <Button Content="Launch Flyout on this button" Flyout="{StaticResource SharedMenuFlyout}"/>
+                    </StackPanel>
+                </Border>
+                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                    <TextBlock Name="SharedFlyoutXamlText" Padding="15" />
+                </Panel>
+            </StackPanel>
+
+            <TextBlock FontSize="18" Text="Flyout Placements" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <UniformGrid Columns="3">
+                        <UniformGrid.Styles>
+                            <Style Selector="Button">
+                                <Setter Property="Margin" Value="10" />
+                            </Style>
+                        </UniformGrid.Styles>
+                        <Button Content="Placement=Top">
+                            <Button.Flyout>
+                                <Flyout Placement="Top">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=Bottom">
+                            <Button.Flyout>
+                                <Flyout Placement="Bottom">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=Left">
+                            <Button.Flyout>
+                                <Flyout Placement="Left">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=Right">
+                            <Button.Flyout>
+                                <Flyout Placement="Right">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=TopEdgeAlignedLeft">
+                            <Button.Flyout>
+                                <Flyout Placement="TopEdgeAlignedLeft">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=TopEdgeAlignedRight">
+                            <Button.Flyout>
+                                <Flyout Placement="TopEdgeAlignedRight">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=BottomEdgeAlignedLeft">
+                            <Button.Flyout>
+                                <Flyout Placement="BottomEdgeAlignedLeft">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=BottomEdgeAlignedRight">
+                            <Button.Flyout>
+                                <Flyout Placement="BottomEdgeAlignedRight">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=LeftEdgeAlignedTop">
+                            <Button.Flyout>
+                                <Flyout Placement="LeftEdgeAlignedTop">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=LeftEdgeAlignedBottom">
+                            <Button.Flyout>
+                                <Flyout Placement="LeftEdgeAlignedBottom">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=RightEdgeAlignedBottom">
+                            <Button.Flyout>
+                                <Flyout Placement="RightEdgeAlignedTop">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=RightEdgeAlignedBottom">
+                            <Button.Flyout>
+                                <Flyout Placement="RightEdgeAlignedBottom">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+
+                    </UniformGrid>
+                </Border>
+            </StackPanel>
+
+            <TextBlock FontSize="18" Text="Flyout ShowMode" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <WrapPanel Orientation="Horizontal">
+                        <WrapPanel.Styles>
+                            <Style Selector="Button">
+                                <Setter Property="Margin" Value="4" />
+                            </Style>
+                        </WrapPanel.Styles>
+                        <Button Content="ShowMode=Standard (default)">
+                            <Button.Flyout>
+                                <Flyout>
+                                    <StackPanel Width="200">
+                                        <TextBox />
+                                        <TextBlock Text="Standard ShowMode attempts to focus the Flyout when its opened" TextWrapping="Wrap"/>
+                                    </StackPanel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="ShowMode=Transient">
+                            <Button.Flyout>
+                                <Flyout ShowMode="Transient">
+                                    <StackPanel Width="200">
+                                        <TextBox />
+                                        <TextBlock Text="Transient ShowMode does not focus the Flyout when opened" TextWrapping="Wrap"/>
+                                    </StackPanel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="ShowMode=TransientWithDismissOnPointerMoveAway">
+                            <Button.Flyout>
+                                <Flyout ShowMode="TransientWithDismissOnPointerMoveAway">
+                                    <StackPanel Width="200">
+                                        <TextBox />
+                                        <TextBlock Text="Show in Transient mode (no focus), but closes the Flyout when the pointer moves away" TextWrapping="Wrap"/>
+                                    </StackPanel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        
+                    </WrapPanel>
+                </Border>
+            </StackPanel>
+            
+        </StackPanel>
+    </ScrollViewer>
+    
+</UserControl>

+ 81 - 0
samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs

@@ -0,0 +1,81 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Markup.Xaml;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+    public class FlyoutsPage : UserControl
+    {
+        public FlyoutsPage()
+        {
+            InitializeComponent();
+
+            var afp = this.FindControl<Panel>("AttachedFlyoutPanel");
+            if (afp != null)
+            {
+                afp.DoubleTapped += Afp_DoubleTapped;
+            }
+
+            SetXamlTexts();
+        }
+
+        private void Afp_DoubleTapped(object sender, RoutedEventArgs e)
+        {
+            if (sender is Panel p)
+            {
+                FlyoutBase.ShowAttachedFlyout(p);
+            }
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void SetXamlTexts()
+        {
+            var bfxt = this.FindControl<TextBlock>("ButtonFlyoutXamlText");
+            bfxt.Text = "<Button Content=\"Click me!\">\n" +
+                        "    <Button.Flyout>\n" +
+                        "        <Flyout>\n" +
+                        "            <Panel Width=\"100\" Height=\"100\">\n" +
+                        "                <TextBlock Text=\"Flyout Content!\" />\n" +
+                        "            </Panel>\n" +
+                        "        </Flyout>\n" +
+                        "    </Button.Flyout>\n</Button>";
+
+            var mfxt = this.FindControl<TextBlock>("MenuFlyoutXamlText");
+            mfxt.Text = "<Button Content=\"Click me!\">\n" +
+                    "    <Button.Flyout>\n" +
+                    "        <MenuFlyout>\n" +
+                    "            <MenuItem Header=\"Item 1\">\n" +
+                    "            <MenuItem Header=\"Item 2\">\n" +
+                    "        </MenuFlyout>\n" +
+                    "    </Button.Flyout>\n</Button>";
+
+            var afxt = this.FindControl<TextBlock>("AttachedFlyoutXamlText");
+            afxt.Text = "<Panel Name=\"AttachedFlyoutPanel\">\n" +
+                "    <FlyoutBase.AttachedFlyout>\n" +
+                "        <Flyout>\n" +
+                "            <Panel Height=\"100\">\n" +
+                "                <TextBlock Text=\"Attached Flyout\" />\n" +
+                "            </Panel>\n" +
+                "        </Flyout>\n" +
+                "    </FlyoutBase.AttachedFlyout>\n</Panel>" + 
+                "\n\n In DoubleTapped handler:\n" +
+                "FlyoutBase.ShowAttachedFlyout(AttachedFlyoutPanel);";
+
+            var sfxt = this.FindControl<TextBlock>("SharedFlyoutXamlText");
+            sfxt.Text = "Declare a flyout in Resources:\n" +
+                "<Window.Resources>\n" +
+                "    <Flyout x:Key=\"SharedFlyout\">\n" +
+                "        <Panel Width=\"100\" Height=\"100\">\n" +
+                "            <TextBlock Text=\"Flyout Content!\" />\n" +
+                "        </Panel>\n" +
+                "    </Flyout>\n</Window.Resources>\n\n" +
+                "Then attach the flyout where you want it:\n" +
+                "<Button Content=\"Launch Flyout here\" Flyout=\"{StaticResource SharedFlyout}\" />";
+        }
+    }
+}

+ 1 - 1
samples/ControlCatalog/Pages/TextBoxPage.xaml

@@ -13,7 +13,7 @@
       <StackPanel Orientation="Vertical" Spacing="8">
         <TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" />
         <TextBox Width="200" Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
-        <TextBox Width="200" Watermark="Watermark" />
+        <TextBox Width="200" Watermark="Numeric Watermark" x:Name="numericWatermark"/>
         <TextBox Width="200"
                  Watermark="Floating Watermark"
                  UseFloatingWatermark="True"

+ 6 - 0
samples/ControlCatalog/Pages/TextBoxPage.xaml.cs

@@ -13,6 +13,12 @@ namespace ControlCatalog.Pages
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
+
+            this.Get<TextBox>("numericWatermark")
+                .TextInputOptionsQuery += (s, a) =>
+                {
+                    a.ContentType = Avalonia.Input.TextInput.TextInputContentType.Number;
+                };
         }
     }
 }

+ 78 - 0
samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs

@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Reactive;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.VisualTree;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+    public class ContextFlyoutPageViewModel
+    {
+        public Control View { get; set; }
+        public ContextFlyoutPageViewModel()
+        {
+            OpenCommand = MiniCommand.CreateFromTask(Open);
+            SaveCommand = MiniCommand.Create(Save);
+            OpenRecentCommand = MiniCommand.Create<string>(OpenRecent);
+
+            MenuItems = new[]
+            {
+                new MenuItemViewModel { Header = "_Open...", Command = OpenCommand },
+                new MenuItemViewModel { Header = "Save", Command = SaveCommand },
+                new MenuItemViewModel { Header = "-" },
+                new MenuItemViewModel
+                {
+                    Header = "Recent",
+                    Items = new[]
+                    {
+                        new MenuItemViewModel
+                        {
+                            Header = "File1.txt",
+                            Command = OpenRecentCommand,
+                            CommandParameter = @"c:\foo\File1.txt"
+                        },
+                        new MenuItemViewModel
+                        {
+                            Header = "File2.txt",
+                            Command = OpenRecentCommand,
+                            CommandParameter = @"c:\foo\File2.txt"
+                        },
+                    }
+                },
+            };
+        }
+
+        public IReadOnlyList<MenuItemViewModel> MenuItems { get; set; }
+        public MiniCommand OpenCommand { get; }
+        public MiniCommand SaveCommand { get; }
+        public MiniCommand OpenRecentCommand { get; }
+
+        public async Task Open()
+        {
+            var window = View?.GetVisualRoot() as Window;
+            if (window == null)
+                return;
+            var dialog = new OpenFileDialog();
+            var result = await dialog.ShowAsync(window);
+
+            if (result != null)
+            {
+                foreach (var path in result)
+                {
+                    System.Diagnostics.Debug.WriteLine($"Opened: {path}");
+                }
+            }
+        }
+
+        public void Save()
+        {
+            System.Diagnostics.Debug.WriteLine("Save");
+        }
+
+        public void OpenRecent(string path)
+        {
+            System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
+        }
+    }
+}

+ 96 - 0
src/Android/Avalonia.Android/AndroidInputMethod.cs

@@ -0,0 +1,96 @@
+using System;
+using Android.Content;
+using Android.Runtime;
+using Android.Views;
+using Android.Views.InputMethods;
+using Avalonia.Input;
+using Avalonia.Input.TextInput;
+
+namespace Avalonia.Android
+{
+    class AndroidInputMethod<TView> : ITextInputMethodImpl
+        where TView: View, IInitEditorInfo
+    {
+        private readonly TView _host;
+        private readonly InputMethodManager _imm;
+        private IInputElement _inputElement;
+
+        public AndroidInputMethod(TView host)
+        {
+            if (host.OnCheckIsTextEditor() == false)
+                throw new InvalidOperationException("Host should return true from OnCheckIsTextEditor()");
+
+            _host = host;
+            _imm = host.Context.GetSystemService(Context.InputMethodService).JavaCast<InputMethodManager>();
+
+            _host.Focusable = true;
+            _host.FocusableInTouchMode = true;
+            _host.ViewTreeObserver.AddOnGlobalLayoutListener(new SoftKeyboardListner(_host));
+        }
+
+        public void Reset()
+        {
+            _imm.RestartInput(_host);
+        }
+
+        public void SetActive(bool active)
+        {
+            if (active)
+            {
+                _host.RequestFocus();
+                Reset();
+                _imm.ShowSoftInput(_host, ShowFlags.Implicit);
+            }
+            else
+                _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None);
+        }
+
+        public void SetCursorRect(Rect rect)
+        {
+        }
+
+        public void SetOptions(TextInputOptionsQueryEventArgs options)
+        {
+            if (_inputElement != null)
+            {
+                _inputElement.PointerReleased -= RestoreSoftKeyboard;
+            }
+
+            _inputElement = options.Source as InputElement;
+
+            if (_inputElement == null)
+            {
+                _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None);
+            }
+
+            _host.InitEditorInfo((outAttrs) =>
+            {
+                outAttrs.InputType = options.ContentType switch
+                {
+                    TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress,
+                    TextInputContentType.Number => global::Android.Text.InputTypes.ClassNumber,
+                    TextInputContentType.Password => global::Android.Text.InputTypes.TextVariationPassword,
+                    TextInputContentType.Phone => global::Android.Text.InputTypes.ClassPhone,
+                    TextInputContentType.Url => global::Android.Text.InputTypes.TextVariationUri,
+                    _ => global::Android.Text.InputTypes.ClassText
+                };
+
+                if (options.AutoCapitalization)
+                {
+                    outAttrs.InitialCapsMode = global::Android.Text.CapitalizationMode.Sentences;
+                    outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagCapSentences;
+                }
+
+                if (options.Multiline)
+                    outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine;
+            });
+
+            //_inputElement.PointerReleased += RestoreSoftKeyboard;
+        }
+
+        private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e)
+        {
+            _imm.ShowSoftInput(_host, ShowFlags.Implicit);
+        }
+    }
+}

+ 0 - 4
src/Android/Avalonia.Android/AvaloniaActivity.cs

@@ -15,7 +15,6 @@ namespace Avalonia.Android
             if (_content != null)
                 View.Content = _content;
             SetContentView(View);
-            TakeKeyEvents(true);
             base.OnCreate(savedInstanceState);
         }
 
@@ -32,8 +31,5 @@ namespace Avalonia.Android
                     View.Content = value;
             }
         }
-
-        public override bool DispatchKeyEvent(KeyEvent e) =>
-            View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e);
     }
 }

+ 12 - 0
src/Android/Avalonia.Android/IInitEditorInfo.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Android.Views.InputMethods;
+
+namespace Avalonia.Android
+{
+    interface IInitEditorInfo
+    {
+        void InitEditorInfo(Action<EditorInfo> init);
+    }
+}

+ 1 - 1
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@@ -32,7 +32,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4);
             Address = buffer.bits;
 
-            Dpi = scaling * new Vector(96, 96);
+            Dpi = new Vector(96, 96) * scaling;
         }
 
         public void Dispose()

+ 1 - 1
src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Android
         bool _invalidateQueued;
         readonly object _lock = new object();
         private readonly Handler _handler;
-        
+   
 
         public InvalidationAwareSurfaceView(Context context) : base(context)
         {

+ 30 - 16
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -4,14 +4,16 @@ using Android.Content;
 using Android.Graphics;
 using Android.Runtime;
 using Android.Views;
-
+using Android.Views.InputMethods;
 using Avalonia.Android.OpenGL;
 using Avalonia.Android.Platform.Specific;
 using Avalonia.Android.Platform.Specific.Helpers;
 using Avalonia.Controls;
+using Avalonia.Controls.Platform;
 using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
 using Avalonia.OpenGL.Egl;
 using Avalonia.OpenGL.Surfaces;
 using Avalonia.Platform;
@@ -19,19 +21,20 @@ using Avalonia.Rendering;
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
-    class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo
+    class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod
     {
         private readonly IGlPlatformSurface _gl;
         private readonly IFramebufferPlatformSurface _framebuffer;
 
         private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
         private readonly AndroidTouchEventsHelper<TopLevelImpl> _touchHelper;
-
+        private readonly ITextInputMethodImpl _textInputMethod;
         private ViewImpl _view;
 
         public TopLevelImpl(Context context, bool placeOnTop = false)
         {
             _view = new ViewImpl(context, this, placeOnTop);
+            _textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
             _keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
             _touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot,
                 GetAvaloniaPointFromEvent);
@@ -45,18 +48,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
                 _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
         }
 
-        private bool _handleEvents;
-
-        public bool HandleEvents
-        {
-            get { return _handleEvents; }
-            set
-            {
-                _handleEvents = value;
-                _keyboardHelper.HandleEvents = _handleEvents;
-            }
-        }
-
         public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
             new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling;
 
@@ -144,7 +135,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             Resized?.Invoke(size);
         }
 
-        class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback
+        class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo
         {
             private readonly TopLevelImpl _tl;
             private Size _oldSize;
@@ -191,6 +182,27 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
                 base.SurfaceChanged(holder, format, width, height);
             }
+
+            public sealed override bool OnCheckIsTextEditor()
+            {
+                return true;
+            }
+
+            private Action<EditorInfo> _initEditorInfo;
+
+            public void InitEditorInfo(Action<EditorInfo> init)
+            {
+                _initEditorInfo = init;
+            }
+
+            public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs)
+            {
+                if (_initEditorInfo != null)
+                    _initEditorInfo(outAttrs);
+
+                return base.OnCreateInputConnection(outAttrs);
+            }
+
         }
 
         public IPopupImpl CreatePopup() => null;
@@ -209,6 +221,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public double Scaling => RenderScaling;
 
+        public ITextInputMethodImpl TextInputMethod => _textInputMethod;
+
         public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
         {
             throw new NotImplementedException();

+ 17 - 66
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs

@@ -1,12 +1,7 @@
 using System;
-using System.ComponentModel;
-using Android.Content;
-using Android.Runtime;
 using Android.Views;
-using Android.Views.InputMethods;
 using Avalonia.Android.Platform.Input;
 using Avalonia.Android.Platform.SkiaPlatform;
-using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 
@@ -14,14 +9,13 @@ namespace Avalonia.Android.Platform.Specific.Helpers
 {
     internal class AndroidKeyboardEventsHelper<TView> : IDisposable where TView : TopLevelImpl, IAndroidView
     {
-        private TView _view;
-        private IInputElement _lastFocusedElement;
+        private readonly TView _view;
 
         public bool HandleEvents { get; set; }
 
         public AndroidKeyboardEventsHelper(TView view)
         {
-            this._view = view;
+            _view = view;
             HandleEvents = true;
         }
 
@@ -36,9 +30,20 @@ namespace Avalonia.Android.Platform.Specific.Helpers
             return DispatchKeyEventInternal(e, out callBase);
         }
 
+        string? UnicodeTextInput(KeyEvent keyEvent)
+        {
+            return keyEvent.Action == KeyEventActions.Multiple
+                && keyEvent.RepeatCount == 0
+                && !string.IsNullOrEmpty(keyEvent?.Characters)
+                ? keyEvent.Characters
+                : null;
+        }
+
         private bool? DispatchKeyEventInternal(KeyEvent e, out bool callBase)
         {
-            if (e.Action == KeyEventActions.Multiple)
+            var unicodeTextInput = UnicodeTextInput(e);
+
+            if (e.Action == KeyEventActions.Multiple && unicodeTextInput == null)
             {
                 callBase = true;
                 return null;
@@ -53,13 +58,14 @@ namespace Avalonia.Android.Platform.Specific.Helpers
 
             _view.Input(rawKeyEvent);
 
-            if (e.Action == KeyEventActions.Down && e.UnicodeChar >= 32)
+            if ((e.Action == KeyEventActions.Down && e.UnicodeChar >= 32)
+                || unicodeTextInput != null)
             {
                 var rawTextEvent = new RawTextInputEventArgs(
                   AndroidKeyboardDevice.Instance,
                   Convert.ToUInt32(e.EventTime),
                   _view.InputRoot,
-                  Convert.ToChar(e.UnicodeChar).ToString()
+                  unicodeTextInput ?? Convert.ToChar(e.UnicodeChar).ToString()
                   );
                 _view.Input(rawTextEvent);
             }
@@ -85,61 +91,6 @@ namespace Avalonia.Android.Platform.Specific.Helpers
             return rv;
         }
 
-        private bool NeedsKeyboard(IInputElement element)
-        {
-            //may be some other elements
-            return element is TextBox;
-        }
-
-        private void TryShowHideKeyboard(IInputElement element, bool value)
-        {
-            var input = _view.View.Context.GetSystemService(Context.InputMethodService).JavaCast<InputMethodManager>();
-
-            if (value)
-            {
-                //show keyboard
-                //may be in the future different keyboards support e.g. normal, only digits etc.
-                //Android.Text.InputTypes
-                input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.ImplicitOnly);
-            }
-            else
-            {
-                //hide keyboard
-                input.HideSoftInputFromWindow(_view.View.WindowToken, HideSoftInputFlags.None);
-            }
-        }
-
-        public void UpdateKeyboardState(IInputElement element)
-        {
-            var focusedElement = element;
-            bool oldValue = NeedsKeyboard(_lastFocusedElement);
-            bool newValue = NeedsKeyboard(focusedElement);
-
-            if (newValue != oldValue || newValue)
-            {
-                TryShowHideKeyboard(focusedElement, newValue);
-            }
-
-            _lastFocusedElement = element;
-        }
-
-        public void ActivateAutoShowKeyboard()
-        {
-            var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged);
-
-            //just in case we've called more than once the method
-            kbDevice.PropertyChanged -= KeyboardDevice_PropertyChanged;
-            kbDevice.PropertyChanged += KeyboardDevice_PropertyChanged;
-        }
-
-        private void KeyboardDevice_PropertyChanged(object sender, PropertyChangedEventArgs e)
-        {
-            if (e.PropertyName == nameof(KeyboardDevice.FocusedElement))
-            {
-                UpdateKeyboardState(KeyboardDevice.Instance.FocusedElement);
-            }
-        }
-
         public void Dispose()
         {
             HandleEvents = false;

+ 42 - 0
src/Android/Avalonia.Android/SoftKeyboardListner.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Android.Content;
+using Android.OS;
+using Android.Util;
+using Android.Views;
+using Avalonia.Input;
+
+namespace Avalonia.Android
+{
+    class SoftKeyboardListner : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
+    {
+        private const int DefaultKeyboardHeightDP = 100;
+        private static readonly int EstimatedKeyboardDP = DefaultKeyboardHeightDP + (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop ? 48 : 0);
+
+        private readonly View _host;
+        private bool _wasKeyboard;
+
+        public SoftKeyboardListner(View view)
+        {
+            _host = view;
+        }
+
+        public void OnGlobalLayout()
+        {
+            int estimatedKeyboardHeight = (int)TypedValue.ApplyDimension(ComplexUnitType.Dip,
+                EstimatedKeyboardDP, _host.Resources.DisplayMetrics);
+
+            var rect = new global::Android.Graphics.Rect();
+            _host.GetWindowVisibleDisplayFrame(rect);
+
+            int heightDiff = _host.RootView.Height - (rect.Bottom - rect.Top);
+            var isKeyboard = heightDiff >= estimatedKeyboardHeight;
+
+            if (_wasKeyboard && !isKeyboard)
+                KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
+
+            _wasKeyboard = isKeyboard;
+        }
+    }
+}

+ 24 - 4
src/Android/Avalonia.AndroidTestApplication/MainActivity.cs

@@ -16,18 +16,18 @@ namespace Avalonia.AndroidTestApplication
         Icon = "@drawable/icon",
         LaunchMode = LaunchMode.SingleInstance/*,
         ScreenOrientation = ScreenOrientation.Landscape*/)]
-    public class MainBaseActivity : Activity
+    public class MainBaseActivity : AvaloniaActivity
     {
         protected override void OnCreate(Bundle savedInstanceState)
         {
-            base.OnCreate(savedInstanceState);
             if (Avalonia.Application.Current == null)
             {
                 AppBuilder.Configure<App>()
                     .UseAndroid()
                     .SetupWithoutStarting();
             }
-            SetContentView(new AvaloniaView(this) { Content = App.CreateSimpleWindow() });
+            base.OnCreate(savedInstanceState);
+            Content = App.CreateSimpleWindow();
         }
     }
 
@@ -72,13 +72,33 @@ namespace Avalonia.AndroidTestApplication
                             Height = 40,
                             Background = Brushes.LightGreen,
                             Foreground = Brushes.Black
-                        }
+                        },
 
+                        CreateTextBox(Input.TextInput.TextInputContentType.Normal),
+                        CreateTextBox(Input.TextInput.TextInputContentType.Password),
+                        CreateTextBox(Input.TextInput.TextInputContentType.Email),
+                        CreateTextBox(Input.TextInput.TextInputContentType.Url),
+                        CreateTextBox(Input.TextInput.TextInputContentType.Phone),
+                        CreateTextBox(Input.TextInput.TextInputContentType.Number),
                     }
                 }
             };
 
             return window;
         }
+
+        private static TextBox CreateTextBox(Input.TextInput.TextInputContentType contentType)
+        {
+            var textBox = new TextBox()
+            {
+                Margin = new Thickness(20, 10),
+                Watermark = contentType.ToString(),
+                BorderThickness = new Thickness(3),
+                FontSize = 20
+            };
+            textBox.TextInputOptionsQuery += (s, e) => { e.ContentType = contentType; };
+
+            return textBox;
+        }
     }
 }

+ 2 - 2
src/Avalonia.Base/Data/BindingValue.cs

@@ -108,12 +108,12 @@ namespace Avalonia.Data
         /// Gets a value indicating whether the binding value represents either a binding or data
         /// validation error.
         /// </summary>
-        public bool HasError => Type.HasFlagCustom(BindingValueType.HasError);
+        public bool HasError => Type.HasAllFlags(BindingValueType.HasError);
 
         /// <summary>
         /// Gets a value indicating whether the binding value has a value.
         /// </summary>
-        public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue);
+        public bool HasValue => Type.HasAllFlags(BindingValueType.HasValue);
 
         /// <summary>
         /// Gets the type of the binding value.

+ 45 - 9
src/Avalonia.Base/EnumExtensions.cs

@@ -9,31 +9,67 @@ namespace Avalonia
     public static class EnumExtensions
     {
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public static unsafe bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum
+        [Obsolete("This method is obsolete. Use HasAllFlags instead.")]
+        public static bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum
+            => value.HasAllFlags(flag);
+            
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static unsafe bool HasAllFlags<T>(this T value, T flags) where T : unmanaged, Enum
+        {
+            if (sizeof(T) == 1)
+            {
+                var byteValue = Unsafe.As<T, byte>(ref value);
+                var byteFlags = Unsafe.As<T, byte>(ref flags);
+                return (byteValue & byteFlags) == byteFlags;
+            }
+            else if (sizeof(T) == 2)
+            {
+                var shortValue = Unsafe.As<T, short>(ref value);
+                var shortFlags = Unsafe.As<T, short>(ref flags);
+                return (shortValue & shortFlags) == shortFlags;
+            }
+            else if (sizeof(T) == 4)
+            {
+                var intValue = Unsafe.As<T, int>(ref value);
+                var intFlags = Unsafe.As<T, int>(ref flags);
+                return (intValue & intFlags) == intFlags;
+            }
+            else if (sizeof(T) == 8)
+            {
+                var longValue = Unsafe.As<T, long>(ref value);
+                var longFlags = Unsafe.As<T, long>(ref flags);
+                return (longValue & longFlags) == longFlags;
+            }
+            else
+                throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf<T>() + " are not supported");
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static unsafe bool HasAnyFlag<T>(this T value, T flags) where T : unmanaged, Enum
         {
             if (sizeof(T) == 1)
             {
                 var byteValue = Unsafe.As<T, byte>(ref value);
-                var byteFlag = Unsafe.As<T, byte>(ref flag);
-                return (byteValue & byteFlag) == byteFlag;
+                var byteFlags = Unsafe.As<T, byte>(ref flags);
+                return (byteValue & byteFlags) != 0;
             }
             else if (sizeof(T) == 2)
             {
                 var shortValue = Unsafe.As<T, short>(ref value);
-                var shortFlag = Unsafe.As<T, short>(ref flag);
-                return (shortValue & shortFlag) == shortFlag;
+                var shortFlags = Unsafe.As<T, short>(ref flags);
+                return (shortValue & shortFlags) != 0;
             }
             else if (sizeof(T) == 4)
             {
                 var intValue = Unsafe.As<T, int>(ref value);
-                var intFlag = Unsafe.As<T, int>(ref flag);
-                return (intValue & intFlag) == intFlag;
+                var intFlags = Unsafe.As<T, int>(ref flags);
+                return (intValue & intFlags) != 0;
             }
             else if (sizeof(T) == 8)
             {
                 var longValue = Unsafe.As<T, long>(ref value);
-                var longFlag = Unsafe.As<T, long>(ref flag);
-                return (longValue & longFlag) == longFlag;
+                var longFlags = Unsafe.As<T, long>(ref flags);
+                return (longValue & longFlags) != 0;
             }
             else
                 throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf<T>() + " are not supported");

+ 2 - 2
src/Avalonia.Base/Utilities/TypeUtilities.cs

@@ -372,8 +372,8 @@ namespace Avalonia.Utilities
             const string implicitName = "op_Implicit";
             const string explicitName = "op_Explicit";
 
-            bool allowImplicit = operatorType.HasFlagCustom(OperatorType.Implicit);
-            bool allowExplicit = operatorType.HasFlagCustom(OperatorType.Explicit);
+            bool allowImplicit = operatorType.HasAllFlags(OperatorType.Implicit);
+            bool allowExplicit = operatorType.HasAllFlags(OperatorType.Explicit);
 
             foreach (MethodInfo method in fromType.GetMethods())
             {

+ 1 - 1
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@@ -2595,7 +2595,7 @@ namespace Avalonia.Collections
         /// <returns>Whether the specified flag is set</returns>
         private bool CheckFlag(CollectionViewFlags flags)
         {
-            return _flags.HasFlagCustom(flags);
+            return _flags.HasAllFlags(flags);
         }
 
         /// <summary>

+ 5 - 0
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -910,6 +910,11 @@ namespace Avalonia.Controls
 
                     // Clear all row selections
                     ClearRowSelection(resetAnchorSlot: true);
+
+                    if (DataConnection.CollectionView != null)
+                    {
+                        DataConnection.CollectionView.MoveCurrentTo(null);
+                    }
                 }
                 else
                 {

+ 51 - 0
src/Avalonia.Controls/Button.cs

@@ -2,6 +2,7 @@ using System;
 using System.Linq;
 using System.Windows.Input;
 using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
@@ -78,6 +79,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> IsPressedProperty =
             AvaloniaProperty.Register<Button, bool>(nameof(IsPressed));
 
+        /// <summary>
+        /// Defines the <see cref="Flyout"/> property
+        /// </summary>
+        public static readonly StyledProperty<FlyoutBase> FlyoutProperty =
+            AvaloniaProperty.Register<Button, FlyoutBase>(nameof(Flyout));
+
         private ICommand _command;
         private bool _commandCanExecute = true;
         private KeyGesture _hotkey;
@@ -89,6 +96,7 @@ namespace Avalonia.Controls
         {
             FocusableProperty.OverrideDefaultValue(typeof(Button), true);
             CommandProperty.Changed.Subscribe(CommandChanged);
+            CommandParameterProperty.Changed.Subscribe(CommandParameterChanged);
             IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
             IsCancelProperty.Changed.Subscribe(IsCancelChanged);
         }
@@ -169,6 +177,15 @@ namespace Avalonia.Controls
             private set { SetValue(IsPressedProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the Flyout that should be shown with this button
+        /// </summary>
+        public FlyoutBase Flyout
+        {
+            get => GetValue(FlyoutProperty);
+            set => SetValue(FlyoutProperty, value);
+        }
+
         protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; 
 
         /// <inheritdoc/>
@@ -256,6 +273,11 @@ namespace Avalonia.Controls
                 IsPressed = true;
                 e.Handled = true;
             }
+            else if (e.Key == Key.Escape && Flyout != null)
+            {
+                // If Flyout doesn't have focusable content, close the flyout here
+                Flyout.Hide();
+            }
 
             base.OnKeyDown(e);
         }
@@ -279,6 +301,8 @@ namespace Avalonia.Controls
         /// </summary>
         protected virtual void OnClick()
         {
+            OpenFlyout();
+
             var e = new RoutedEventArgs(ClickEvent);
             RaiseEvent(e);
 
@@ -289,6 +313,11 @@ namespace Avalonia.Controls
             }
         }
 
+        protected virtual void OpenFlyout()
+        {
+            Flyout?.ShowAt(this);
+        }
+
         /// <inheritdoc/>
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
@@ -337,6 +366,16 @@ namespace Avalonia.Controls
             {
                 UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
             }
+            else if (change.Property == FlyoutProperty)
+            {
+                // If flyout is changed while one is already open, make sure we 
+                // close the old one first
+                if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout &&
+                    oldFlyout.IsOpen)
+                {
+                    oldFlyout.Hide();
+                }
+            }
         }
 
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
@@ -380,6 +419,18 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Called when the <see cref="CommandParameter"/> property changes.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is Button button)
+            {
+                button.CanExecuteChanged(button, EventArgs.Empty);
+            }
+        }
+
         /// <summary>
         /// Called when the <see cref="IsDefault"/> property changes.
         /// </summary>

+ 1 - 1
src/Avalonia.Controls/ComboBox.cs

@@ -206,7 +206,7 @@ namespace Avalonia.Controls
                 return;
 
             if (e.Key == Key.F4 ||
-                ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt)))
+                ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt)))
             {
                 IsDropDownOpen = !IsDropDownOpen;
                 e.Handled = true;

+ 22 - 0
src/Avalonia.Controls/Control.cs

@@ -37,9 +37,16 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="ContextMenu"/> property.
         /// </summary>
+        [Obsolete("Prefer ContextFlyout")]
         public static readonly StyledProperty<ContextMenu?> ContextMenuProperty =
             AvaloniaProperty.Register<Control, ContextMenu?>(nameof(ContextMenu));
 
+        /// <summary>
+        /// Defines the <see cref="ContextFlyout"/> property
+        /// </summary>
+        public static readonly StyledProperty<FlyoutBase?> ContextFlyoutProperty =
+            AvaloniaProperty.Register<Control, FlyoutBase?>(nameof(ContextFlyout));
+
         /// <summary>
         /// Event raised when an element wishes to be scrolled into view.
         /// </summary>
@@ -70,12 +77,22 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets a context menu to the control.
         /// </summary>
+        [Obsolete("Prefer ContextFlyout")]
         public ContextMenu? ContextMenu
         {
             get => GetValue(ContextMenuProperty);
             set => SetValue(ContextMenuProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets a context flyout to the control
+        /// </summary>
+        public FlyoutBase? ContextFlyout
+        {
+            get => GetValue(ContextFlyoutProperty);
+            set => SetValue(ContextFlyoutProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets a user-defined object attached to the control.
         /// </summary>
@@ -93,6 +110,11 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         void ISetterValue.Initialize(ISetter setter)
         {
+            if (setter is Setter s && s.Property == ContextFlyoutProperty)
+            {
+                return; // Allow ContextFlyout to not need wrapping in <Template>
+            }
+
             throw new InvalidOperationException(
                 "Cannot use a control as a Setter value. Wrap the control in a <Template>.");
         }

+ 8 - 8
src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs

@@ -72,24 +72,24 @@ namespace Avalonia.Controls.Converters
                 }
             }
 
-            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+            if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control))
             {
                 s.Append("Ctrl");
             }
 
-            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+            if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Shift))
             {
                 Plus(s);
                 s.Append("Shift");
             }
 
-            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+            if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Alt))
             {
                 Plus(s);
                 s.Append("Alt");
             }
 
-            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+            if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
             {
                 Plus(s);
                 s.Append(meta);
@@ -105,22 +105,22 @@ namespace Avalonia.Controls.Converters
         {
             var s = new StringBuilder();
 
-            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+            if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control))
             {
                 s.Append('⌃');
             }
 
-            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+            if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Alt))
             {
                 s.Append('⌥');
             }
 
-            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+            if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Shift))
             {
                 s.Append('⇧');
             }
 
-            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+            if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
             {
                 s.Append('⌘');
             }

+ 50 - 0
src/Avalonia.Controls/Flyouts/Flyout.cs

@@ -0,0 +1,50 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Metadata;
+
+#nullable enable
+
+namespace Avalonia.Controls
+{
+    public class Flyout : FlyoutBase
+    {
+        /// <summary>
+        /// Defines the <see cref="Content"/> property
+        /// </summary>
+        public static readonly StyledProperty<object> ContentProperty =
+            AvaloniaProperty.Register<Flyout, object>(nameof(Content));
+
+        /// <summary>
+        /// Gets the Classes collection to apply to the FlyoutPresenter this Flyout is hosting
+        /// </summary>
+        public Classes FlyoutPresenterClasses => _classes ??= new Classes();
+
+        private Classes? _classes;
+
+        /// <summary>
+        /// Gets or sets the content to display in this flyout
+        /// </summary>
+        [Content]
+        public object Content
+        {
+            get => GetValue(ContentProperty);
+            set => SetValue(ContentProperty, value);
+        }
+
+        protected override Control CreatePresenter()
+        {
+            return new FlyoutPresenter
+            {
+                [!ContentControl.ContentProperty] = this[!ContentProperty]
+            };
+        }
+
+        protected override void OnOpened()
+        {
+            if (_classes != null)
+            {
+                SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
+            }
+            base.OnOpened();
+        }
+    }
+}

+ 504 - 0
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@@ -0,0 +1,504 @@
+using System;
+using System.ComponentModel;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Layout;
+using Avalonia.Logging;
+
+#nullable enable
+
+namespace Avalonia.Controls.Primitives
+{
+    public abstract class FlyoutBase : AvaloniaObject
+    {
+        static FlyoutBase()
+        {
+            Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
+        }
+
+        /// <summary>
+        /// Defines the <see cref="IsOpen"/> property
+        /// </summary>
+        private static readonly DirectProperty<FlyoutBase, bool> IsOpenProperty =
+           AvaloniaProperty.RegisterDirect<FlyoutBase, bool>(nameof(IsOpen),
+               x => x.IsOpen);
+
+        /// <summary>
+        /// Defines the <see cref="Target"/> property
+        /// </summary>
+        public static readonly DirectProperty<FlyoutBase, Control?> TargetProperty =
+            AvaloniaProperty.RegisterDirect<FlyoutBase, Control?>(nameof(Target), x => x.Target);
+
+        /// <summary>
+        /// Defines the <see cref="Placement"/> property
+        /// </summary>
+        public static readonly StyledProperty<FlyoutPlacementMode> PlacementProperty =
+            AvaloniaProperty.Register<FlyoutBase, FlyoutPlacementMode>(nameof(Placement));
+
+        /// <summary>
+        /// Defines the <see cref="ShowMode"/> property
+        /// </summary>
+        public static readonly DirectProperty<FlyoutBase, FlyoutShowMode> ShowModeProperty =
+            AvaloniaProperty.RegisterDirect<FlyoutBase, FlyoutShowMode>(nameof(ShowMode),
+                x => x.ShowMode, (x, v) => x.ShowMode = v);
+
+        /// <summary>
+        /// Defines the AttachedFlyout property
+        /// </summary>
+        public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty =
+            AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null);
+
+        private bool _isOpen;
+        private Control? _target;
+        private FlyoutShowMode _showMode = FlyoutShowMode.Standard;
+        private Rect? enlargedPopupRect;
+        private IDisposable? transientDisposable;
+
+        protected Popup? Popup { get; private set; }
+
+        /// <summary>
+        /// Gets whether this Flyout is currently Open
+        /// </summary>
+        public bool IsOpen
+        {
+            get => _isOpen;
+            private set => SetAndRaise(IsOpenProperty, ref _isOpen, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the desired placement
+        /// </summary>
+        public FlyoutPlacementMode Placement
+        {
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the desired ShowMode
+        /// </summary>
+        public FlyoutShowMode ShowMode
+        {
+            get => _showMode;
+            set => SetAndRaise(ShowModeProperty, ref _showMode, value);
+        }
+
+        /// <summary>
+        /// Gets the Target used for showing the Flyout
+        /// </summary>
+        public Control? Target
+        {
+            get => _target;
+            private set => SetAndRaise(TargetProperty, ref _target, value);
+        }
+
+        public event EventHandler? Closed;
+        public event EventHandler<CancelEventArgs>? Closing;
+        public event EventHandler? Opened;
+        public event EventHandler? Opening;
+
+        public static FlyoutBase? GetAttachedFlyout(Control element)
+        {
+            return element.GetValue(AttachedFlyoutProperty);
+        }
+
+        public static void SetAttachedFlyout(Control element, FlyoutBase? value)
+        {
+            element.SetValue(AttachedFlyoutProperty, value);
+        }
+
+        public static void ShowAttachedFlyout(Control flyoutOwner)
+        {
+            var flyout = GetAttachedFlyout(flyoutOwner);
+            flyout?.ShowAt(flyoutOwner);
+        }
+
+        /// <summary>
+        /// Shows the Flyout at the given Control
+        /// </summary>
+        /// <param name="placementTarget">The control to show the Flyout at</param>
+        public void ShowAt(Control placementTarget)
+        {
+            ShowAtCore(placementTarget);
+        }
+
+        /// <summary>
+        /// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
+        /// </summary>
+        /// <param name="placementTarget">The target control</param>
+        /// <param name="showAtPointer">True to show at pointer</param>
+        public void ShowAt(Control placementTarget, bool showAtPointer)
+        {
+            ShowAtCore(placementTarget, showAtPointer);
+        }
+
+        /// <summary>
+        /// Hides the Flyout
+        /// </summary>
+        public void Hide()
+        {
+            HideCore();
+        }
+
+        protected virtual void HideCore(bool canCancel = true)
+        {
+            if (!IsOpen)
+            {
+                return;
+            }
+
+            if (canCancel)
+            {
+                bool cancel = false;
+
+                var closing = new CancelEventArgs();
+                Closing?.Invoke(this, closing);
+                if (cancel || closing.Cancel)
+                {
+                    return;
+                }
+            }
+
+            IsOpen = false;
+            Popup.IsOpen = false;
+
+            // Ensure this isn't active
+            transientDisposable?.Dispose();
+            transientDisposable = null;
+
+            OnClosed();
+        }
+
+        protected virtual void ShowAtCore(Control placementTarget, bool showAtPointer = false)
+        {
+            if (placementTarget == null)
+                throw new ArgumentNullException("placementTarget cannot be null");
+
+            if (Popup == null)
+            {
+                InitPopup();
+            }
+
+            if (IsOpen)
+            {
+                if (placementTarget == Target)
+                {
+                    return;
+                }
+                else // Close before opening a new one
+                {
+                    HideCore(false);
+                }
+            }
+
+            if (Popup.Parent != null && Popup.Parent != placementTarget)
+            {
+                ((ISetLogicalParent)Popup).SetParent(null);
+            }
+
+            if (Popup.PlacementTarget != placementTarget)
+            {
+                Popup.PlacementTarget = Target = placementTarget;
+                ((ISetLogicalParent)Popup).SetParent(placementTarget);
+            }
+
+            if (Popup.Child == null)
+            {
+                Popup.Child = CreatePresenter();
+            }
+
+            OnOpening();
+            PositionPopup(showAtPointer);
+            IsOpen = Popup.IsOpen = true;            
+            OnOpened();
+                        
+            if (ShowMode == FlyoutShowMode.Standard)
+            {
+                // Try and focus content inside Flyout
+                if (Popup.Child.Focusable)
+                {
+                    FocusManager.Instance?.Focus(Popup.Child);
+                }
+                else
+                {
+                    var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next);
+                    if (nextFocus != null)
+                    {
+                        FocusManager.Instance?.Focus(nextFocus);
+                    }
+                }
+            }
+            else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway)
+            {
+                transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
+            }
+        }
+
+        private void HandleTransientDismiss(RawInputEventArgs args)
+        {
+            if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move)
+            {
+                // In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
+                // shown as long as the pointer is within a certain px distance from the
+                // flyout itself. I'm not sure what WinUI uses, but I'm defaulting to 
+                // 100px, which seems about right
+                // enlargedPopupRect is the Flyout bounds enlarged 100px
+                // For windowed popups, enlargedPopupRect is in screen coordinates,
+                // for overlay popups, its in OverlayLayer coordinates
+
+                if (enlargedPopupRect == null)
+                {
+                    // Only do this once when the Flyout opens & cache the result
+                    if (Popup?.Host is PopupRoot root)
+                    { 
+                        // Get the popup root bounds and convert to screen coordinates
+                        var tmp = root.Bounds.Inflate(100);
+                        var scPt = root.PointToScreen(tmp.TopLeft);
+                        enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height);
+                    }
+                    else if (Popup?.Host is OverlayPopupHost host)
+                    {
+                        // Overlay popups are in OverlayLayer coordinates, just use that
+                        enlargedPopupRect = host.Bounds.Inflate(100);
+                    }
+
+                    return;
+                }
+
+                if (Popup?.Host is PopupRoot)
+                {
+                    // As long as the pointer stays within the enlargedPopupRect
+                    // the flyout stays open. If it leaves, close it
+                    // Despite working in screen coordinates, leaving the TopLevel
+                    // window will not close this (as pointer events stop), which 
+                    // does match UWP
+                    var pt = pArgs.Root.PointToScreen(pArgs.Position);
+                    if (!enlargedPopupRect?.Contains(new Point(pt.X, pt.Y)) ?? false)
+                    {
+                        HideCore(false);
+                        enlargedPopupRect = null;
+                        transientDisposable?.Dispose();
+                        transientDisposable = null;
+                    }
+                }
+                else if (Popup?.Host is OverlayPopupHost)
+                {
+                    // Same as above here, but just different coordinate space
+                    // so we don't need to translate
+                    if (!enlargedPopupRect?.Contains(pArgs.Position) ?? false)
+                    {
+                        HideCore(false);
+                        enlargedPopupRect = null;
+                        transientDisposable?.Dispose();
+                        transientDisposable = null;
+                    }
+                }
+            }
+        }
+
+        protected virtual void OnOpening()
+        {
+            Opening?.Invoke(this, null);
+        }
+
+        protected virtual void OnOpened()
+        {
+            Opened?.Invoke(this, null);
+        }
+
+        protected virtual void OnClosing(CancelEventArgs args)
+        {
+            Closing?.Invoke(this, args);
+        }
+
+        protected virtual void OnClosed()
+        {
+            Closed?.Invoke(this, null);
+        }
+
+        /// <summary>
+        /// Used to create the content the Flyout displays
+        /// </summary>
+        /// <returns></returns>
+        protected abstract Control CreatePresenter();
+
+        private void InitPopup()
+        {
+            Popup = new Popup();
+            Popup.WindowManagerAddShadowHint = false;
+            Popup.IsLightDismissEnabled = true;
+
+            Popup.Opened += OnPopupOpened;
+            Popup.Closed += OnPopupClosed;
+        }
+
+        private void OnPopupOpened(object sender, EventArgs e)
+        {
+            IsOpen = true;
+        }
+
+        private void OnPopupClosed(object sender, EventArgs e)
+        {
+            HideCore();
+        }
+
+        private void PositionPopup(bool showAtPointer)
+        {
+            Size sz;
+            if(Popup.Child.DesiredSize == Size.Empty)
+            {
+                // Popup may not have been shown yet. Measure content
+                sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
+            }
+            else
+            {
+                sz = Popup.Child.DesiredSize;
+            }
+
+            if (showAtPointer)
+            {
+                Popup.PlacementMode = PlacementMode.Pointer;
+            }
+            else
+            {
+                Popup.PlacementMode = PlacementMode.AnchorAndGravity;
+                Popup.PlacementConstraintAdjustment =
+                    PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
+                    PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;
+            }
+
+            var trgtBnds = Target?.Bounds ?? Rect.Empty;
+
+            switch (Placement)
+            {
+                case FlyoutPlacementMode.Top: //Above & centered
+                    Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width-1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top;
+                    break;
+
+                case FlyoutPlacementMode.TopEdgeAlignedLeft:
+                    Popup.PlacementRect = new Rect(0, 0, 0, 0);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;                    
+                    break;
+
+                case FlyoutPlacementMode.TopEdgeAlignedRight:
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;                    
+                    break;
+
+                case FlyoutPlacementMode.RightEdgeAlignedTop:
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
+                    break;
+
+                case FlyoutPlacementMode.Right: //Right & centered
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, trgtBnds.Height);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Right;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
+                    break;
+
+                case FlyoutPlacementMode.RightEdgeAlignedBottom:
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
+                    break;
+
+                case FlyoutPlacementMode.Bottom: //Below & centered
+                    Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, trgtBnds.Width, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Bottom;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
+                    break;
+
+                case FlyoutPlacementMode.BottomEdgeAlignedLeft:
+                    Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
+                    break;
+
+                case FlyoutPlacementMode.BottomEdgeAlignedRight:
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
+                    break;
+
+                case FlyoutPlacementMode.LeftEdgeAlignedTop:
+                    Popup.PlacementRect = new Rect(0, 0, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
+                    break;
+
+                case FlyoutPlacementMode.Left: //Left & centered
+                    Popup.PlacementRect = new Rect(0, 0, 1, trgtBnds.Height);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Left;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
+                    break;
+
+                case FlyoutPlacementMode.LeftEdgeAlignedBottom:
+                    Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.BottomLeft;
+                    break;
+
+                //includes Auto (not sure what determines that)...
+                default:
+                    //This is just FlyoutPlacementMode.Top behavior (above & centered)
+                    Popup.PlacementRect = new Rect(-sz.Width / 2, 0, sz.Width, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
+                    break;
+            }
+        }
+
+        private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args)
+        {
+            if (args.Sender is Control c)
+            {
+                if (args.OldValue is FlyoutBase)
+                {
+                    c.PointerReleased -= OnControlWithContextFlyoutPointerReleased;
+                }
+                if (args.NewValue is FlyoutBase)
+                {
+                    c.PointerReleased += OnControlWithContextFlyoutPointerReleased;
+                }
+            }
+        }
+
+        private static void OnControlWithContextFlyoutPointerReleased(object sender, PointerReleasedEventArgs e)
+        {
+            if (sender is Control c)
+            {
+                if (e.InitialPressMouseButton == MouseButton.Right &&
+                e.GetCurrentPoint(c).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonReleased)
+                {
+                    if (c.ContextFlyout != null)
+                    {
+                        if (c.ContextMenu != null)
+                        {
+                            Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(c, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
+                            return;
+                        }
+                        c.ContextFlyout.ShowAt(c, true);
+                    }
+                }
+            }            
+        }
+
+        internal static void SetPresenterClasses(IControl presenter, Classes classes)
+        {
+            //Remove any classes no longer in use, ignoring pseudoclasses
+            for (int i = presenter.Classes.Count - 1; i >= 0; i--)
+            {
+                if (!classes.Contains(presenter.Classes[i]) &&
+                    !presenter.Classes[i].Contains(":"))
+                {
+                    presenter.Classes.RemoveAt(i);
+                }
+            }
+
+            //Add new classes
+            presenter.Classes.AddRange(classes);
+        }
+    }
+}

+ 77 - 0
src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs

@@ -0,0 +1,77 @@
+namespace Avalonia.Controls
+{
+    public enum FlyoutPlacementMode
+    {
+        /// <summary>
+        /// Preferred location is above the target element
+        /// </summary>
+        Top = 0,
+
+        /// <summary>
+        /// Preferred location is below the target element
+        /// </summary>
+        Bottom = 1,
+
+        /// <summary>
+        /// Preferred location is to the left of the target element
+        /// </summary>
+        Left = 2,
+
+        /// <summary>
+        /// Preferred location is to the right of the target element
+        /// </summary>
+        Right = 3,
+
+        //TODO
+        // <summary>
+        // Preferred location is centered on the screen
+        // </summary>
+        //Full = 4,
+
+        /// <summary>
+        /// Preferred location is above the target element, with the left edge of the flyout
+        /// aligned with the left edge of the target element
+        /// </summary>
+        TopEdgeAlignedLeft = 5,
+
+        /// <summary>
+        /// Preferred location is above the target element, with the right edge of flyout aligned with right edge of the target element.
+        /// </summary>
+        TopEdgeAlignedRight = 6,
+
+        /// <summary>
+        /// Preferred location is below the target element, with the left edge of flyout aligned with left edge of the target element.
+        /// </summary>
+        BottomEdgeAlignedLeft = 7,
+
+        /// <summary>
+        /// Preferred location is below the target element, with the right edge of flyout aligned with right edge of the target element.
+        /// </summary>
+        BottomEdgeAlignedRight = 8,
+
+        /// <summary>
+        /// Preferred location is to the left of the target element, with the top edge of flyout aligned with top edge of the target element.
+        /// </summary>
+        LeftEdgeAlignedTop = 9,
+
+        /// <summary>
+        /// Preferred location is to the left of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
+        /// </summary>
+        LeftEdgeAlignedBottom = 10,
+
+        /// <summary>
+        /// Preferred location is to the right of the target element, with the top edge of flyout aligned with top edge of the target element.
+        /// </summary>
+        RightEdgeAlignedTop = 11,
+
+        /// <summary>
+        /// Preferred location is to the right of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
+        /// </summary>
+        RightEdgeAlignedBottom = 12,
+
+        /// <summary>
+        /// Preferred location is determined automatically.
+        /// </summary>
+        Auto = 13
+    }
+}

+ 33 - 0
src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs

@@ -0,0 +1,33 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+
+namespace Avalonia.Controls
+{
+    public class FlyoutPresenter : ContentControl
+    {
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
+            Border.CornerRadiusProperty.AddOwner<FlyoutPresenter>();
+
+        public CornerRadius CornerRadius
+        {
+            get => GetValue(CornerRadiusProperty);
+            set => SetValue(CornerRadiusProperty, value);
+        }
+
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            if (e.Key == Key.Escape)
+            {
+                var host = this.FindLogicalAncestorOfType<Popup>();
+                if (host != null)
+                {
+                    host.IsOpen = false;
+                    e.Handled = true;
+                }
+            }
+
+            base.OnKeyDown(e);            
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs

@@ -0,0 +1,24 @@
+namespace Avalonia.Controls
+{
+    // Note: FlyoutShowMode.Auto was removed. MS Docs just say:
+    // The show mode is determined automatically based on the method used to show the flyout.
+    // and AFAICT Flyouts generally open with "Standard" behavior
+
+    public enum FlyoutShowMode
+    {
+        /// <summary>
+        /// Behavior is typical of a flyout shown reactively, like a context menu. The open flyout takes focus. For a CommandBarFlyout, it opens in it's expanded state.
+        /// </summary>
+        Standard,
+
+        /// <summary>
+        /// Behavior is typical of a flyout shown proactively. The open flyout does not take focus. For a CommandBarFlyout, it opens in it's collapsed state.
+        /// </summary>
+        Transient,
+
+        /// <summary>
+        /// The flyout exhibits Transient behavior while the cursor is close to it, but is dismissed when the cursor moves away.
+        /// </summary>
+        TransientWithDismissOnPointerMoveAway
+    }
+}

+ 75 - 0
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@@ -0,0 +1,75 @@
+using System.Collections;
+using Avalonia.Collections;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Metadata;
+
+#nullable enable
+
+namespace Avalonia.Controls
+{
+    public class MenuFlyout : FlyoutBase
+    {
+        public MenuFlyout()
+        {
+            _items = new AvaloniaList<object>();
+        }
+
+        /// <summary>
+        /// Defines the <see cref="Items"/> property
+        /// </summary>
+        public static readonly DirectProperty<MenuFlyout, IEnumerable> ItemsProperty =
+            ItemsControl.ItemsProperty.AddOwner<MenuFlyout>(x => x.Items,
+                (x, v) => x.Items = v);
+
+        /// <summary>
+        /// Defines the <see cref="ItemTemplate"/> property
+        /// </summary>
+        public static readonly DirectProperty<MenuFlyout, IDataTemplate?> ItemTemplateProperty =
+            AvaloniaProperty.RegisterDirect<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate),
+                x => x.ItemTemplate, (x, v) => x.ItemTemplate = v);
+
+        public Classes FlyoutPresenterClasses => _classes ??= new Classes();
+
+        /// <summary>
+        /// Gets or sets the items of the MenuFlyout
+        /// </summary>
+        [Content]
+        public IEnumerable Items
+        {
+            get => _items;
+            set => SetAndRaise(ItemsProperty, ref _items, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the template used for the items
+        /// </summary>
+        public IDataTemplate? ItemTemplate
+        {
+            get => _itemTemplate;
+            set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value);
+        }
+
+        private Classes? _classes;
+        private IEnumerable _items;
+        private IDataTemplate? _itemTemplate;
+
+        protected override Control CreatePresenter()
+        {
+            return new MenuFlyoutPresenter
+            {
+                [!ItemsControl.ItemsProperty] = this[!ItemsProperty],
+                [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty]
+            };
+        }
+
+        protected override void OnOpened()
+        {
+            if (_classes != null)
+            {
+                SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
+            }
+            base.OnOpened();
+        }
+    }
+}

+ 55 - 0
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@@ -0,0 +1,55 @@
+using System;
+using Avalonia.Controls.Generators;
+using Avalonia.Controls.Platform;
+using Avalonia.Controls.Primitives;
+using Avalonia.LogicalTree;
+
+namespace Avalonia.Controls
+{
+    public class MenuFlyoutPresenter : MenuBase
+    {
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
+            Border.CornerRadiusProperty.AddOwner<FlyoutPresenter>();
+
+        public CornerRadius CornerRadius
+        {
+            get => GetValue(CornerRadiusProperty);
+            set => SetValue(CornerRadiusProperty, value);
+        }
+
+        public MenuFlyoutPresenter()
+            :base(new DefaultMenuInteractionHandler(true))
+        {
+
+        }
+
+        public override void Close()
+        {
+            // DefaultMenuInteractionHandler calls this
+            var host = this.FindLogicalAncestorOfType<Popup>();
+            if (host != null)
+            {
+                for (int i = 0; i < LogicalChildren.Count; i++)
+                {
+                    if (LogicalChildren[i] is MenuItem item)
+                    {
+                        item.IsSubMenuOpen = false;
+                    }
+                }
+
+                SelectedIndex = -1;
+                host.IsOpen = false;                
+            }
+        }
+
+        public override void Open()
+        {
+            throw new NotSupportedException("Use MenuFlyout.ShowAt(Control) instead");
+        }
+
+        protected override IItemContainerGenerator CreateItemContainerGenerator()
+        {
+            return new MenuItemContainerGenerator(this);
+        }
+    }
+}

+ 5 - 5
src/Avalonia.Controls/Grid.cs

@@ -2355,7 +2355,7 @@ namespace Avalonia.Controls
         /// </summary>
         private bool CheckFlags(Flags flags)
         {
-            return _flags.HasFlagCustom(flags);
+            return _flags.HasAllFlags(flags);
         }
 
         private static void OnShowGridLinesPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
@@ -2790,10 +2790,10 @@ namespace Avalonia.Controls
             internal LayoutTimeSizeType SizeTypeU;
             internal LayoutTimeSizeType SizeTypeV;
             internal int Next;
-            internal bool IsStarU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Star);
-            internal bool IsAutoU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Auto);
-            internal bool IsStarV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Star);
-            internal bool IsAutoV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Auto);
+            internal bool IsStarU => SizeTypeU.HasAllFlags(LayoutTimeSizeType.Star);
+            internal bool IsAutoU => SizeTypeU.HasAllFlags(LayoutTimeSizeType.Auto);
+            internal bool IsStarV => SizeTypeV.HasAllFlags(LayoutTimeSizeType.Star);
+            internal bool IsAutoV => SizeTypeV.HasAllFlags(LayoutTimeSizeType.Auto);
         }
 
         /// <summary>

+ 4 - 4
src/Avalonia.Controls/ListBox.cs

@@ -135,8 +135,8 @@ namespace Avalonia.Controls
                 e.Handled = UpdateSelectionFromEventSource(
                     e.Source,
                     true,
-                    e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift),
-                    e.KeyModifiers.HasFlagCustom(KeyModifiers.Control));
+                    e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
+                    e.KeyModifiers.HasAllFlags(KeyModifiers.Control));
             }
         }
 
@@ -154,8 +154,8 @@ namespace Avalonia.Controls
                     e.Handled = UpdateSelectionFromEventSource(
                         e.Source,
                         true,
-                        e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift),
-                        e.KeyModifiers.HasFlagCustom(KeyModifiers.Control),
+                        e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
+                        e.KeyModifiers.HasAllFlags(KeyModifiers.Control),
                         point.Properties.IsRightButtonPressed);
                 }
             }

+ 6 - 6
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@@ -73,20 +73,20 @@ namespace Avalonia.Platform
         {
             if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None)
                 return effect; // No need to check for the modifiers.
-            if (effect.HasFlagCustom(DragDropEffects.Link) && modifiers.HasFlagCustom(RawInputModifiers.Alt))
+            if (effect.HasAllFlags(DragDropEffects.Link) && modifiers.HasAllFlags(RawInputModifiers.Alt))
                 return DragDropEffects.Link;
-            if (effect.HasFlagCustom(DragDropEffects.Copy) && modifiers.HasFlagCustom(RawInputModifiers.Control))
+            if (effect.HasAllFlags(DragDropEffects.Copy) && modifiers.HasAllFlags(RawInputModifiers.Control))
                 return DragDropEffects.Copy;
             return DragDropEffects.Move;
         }
 
         private StandardCursorType GetCursorForDropEffect(DragDropEffects effects)
         {
-            if (effects.HasFlagCustom(DragDropEffects.Copy))
+            if (effects.HasAllFlags(DragDropEffects.Copy))
                 return StandardCursorType.DragCopy;
-            if (effects.HasFlagCustom(DragDropEffects.Move))
+            if (effects.HasAllFlags(DragDropEffects.Move))
                 return StandardCursorType.DragMove;
-            if (effects.HasFlagCustom(DragDropEffects.Link))
+            if (effects.HasAllFlags(DragDropEffects.Link))
                 return StandardCursorType.DragLink;
             return StandardCursorType.No;
         }
@@ -161,7 +161,7 @@ namespace Avalonia.Platform
             
             void CheckDraggingAccepted(RawInputModifiers changedMouseButton)
             {
-                if (_initialInputModifiers.Value.HasFlagCustom(changedMouseButton))
+                if (_initialInputModifiers.Value.HasAllFlags(changedMouseButton))
                 {
                     var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers);
                     UpdateCursor(null, DragDropEffects.None);

+ 6 - 6
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@@ -253,8 +253,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
     {
         public static void ValidateEdge(this PopupAnchor edge)
         {
-            if (edge.HasFlagCustom(PopupAnchor.Left) && edge.HasFlagCustom(PopupAnchor.Right) ||
-                edge.HasFlagCustom(PopupAnchor.Top) && edge.HasFlagCustom(PopupAnchor.Bottom))
+            if (edge.HasAllFlags(PopupAnchor.Left | PopupAnchor.Right) ||
+                edge.HasAllFlags(PopupAnchor.Top | PopupAnchor.Bottom))
                 throw new ArgumentException("Opposite edges specified");
         }
 
@@ -265,10 +265,10 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
 
         public static PopupAnchor Flip(this PopupAnchor edge)
         {
-            if (edge.HasFlagCustom(PopupAnchor.HorizontalMask))
+            if (edge.HasAnyFlag(PopupAnchor.HorizontalMask))
                 edge ^= PopupAnchor.HorizontalMask;
 
-            if (edge.HasFlagCustom(PopupAnchor.VerticalMask))
+            if (edge.HasAnyFlag(PopupAnchor.VerticalMask))
                 edge ^= PopupAnchor.VerticalMask;
 
             return edge;
@@ -276,14 +276,14 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
 
         public static PopupAnchor FlipX(this PopupAnchor edge)
         {
-            if (edge.HasFlagCustom(PopupAnchor.HorizontalMask))
+            if (edge.HasAnyFlag(PopupAnchor.HorizontalMask))
                 edge ^= PopupAnchor.HorizontalMask;
             return edge;
         }
         
         public static PopupAnchor FlipY(this PopupAnchor edge)
         {
-            if (edge.HasFlagCustom(PopupAnchor.VerticalMask))
+            if (edge.HasAnyFlag(PopupAnchor.VerticalMask))
                 edge ^= PopupAnchor.VerticalMask;
             return edge;
         }

+ 18 - 18
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@@ -42,16 +42,16 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
         private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge)
         {
             double x, y;
-            if (edge.HasFlagCustom(PopupAnchor.Left))
+            if (edge.HasAllFlags(PopupAnchor.Left))
                 x = anchorRect.X;
-            else if (edge.HasFlagCustom(PopupAnchor.Right))
+            else if (edge.HasAllFlags(PopupAnchor.Right))
                 x = anchorRect.Right;
             else
                 x = anchorRect.X + anchorRect.Width / 2;
             
-            if (edge.HasFlagCustom(PopupAnchor.Top))
+            if (edge.HasAllFlags(PopupAnchor.Top))
                 y = anchorRect.Y;
-            else if (edge.HasFlagCustom(PopupAnchor.Bottom))
+            else if (edge.HasAllFlags(PopupAnchor.Bottom))
                 y = anchorRect.Bottom;
             else
                 y = anchorRect.Y + anchorRect.Height / 2;
@@ -61,16 +61,16 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
         private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity)
         {
             double x, y;
-            if (gravity.HasFlagCustom(PopupGravity.Left))
+            if (gravity.HasAllFlags(PopupGravity.Left))
                 x = -size.Width;
-            else if (gravity.HasFlagCustom(PopupGravity.Right))
+            else if (gravity.HasAllFlags(PopupGravity.Right))
                 x = 0;
             else
                 x = -size.Width / 2;
             
-            if (gravity.HasFlagCustom(PopupGravity.Top))
+            if (gravity.HasAllFlags(PopupGravity.Top))
                 y = -size.Height;
-            else if (gravity.HasFlagCustom(PopupGravity.Bottom))
+            else if (gravity.HasAllFlags(PopupGravity.Bottom))
                 y = 0;
             else
                 y = -size.Height / 2;
@@ -125,10 +125,10 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
 
             bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask)
             {
-                if (edge.HasFlagCustom(PopupAnchor.Left) && rc.X < bounds.X ||
-                    edge.HasFlagCustom(PopupAnchor.Top) && rc.Y < bounds.Y ||
-                    edge.HasFlagCustom(PopupAnchor.Right) && rc.Right > bounds.Right ||
-                    edge.HasFlagCustom(PopupAnchor.Bottom) && rc.Bottom > bounds.Bottom)
+                if (edge.HasAllFlags(PopupAnchor.Left) && rc.X < bounds.X ||
+                    edge.HasAllFlags(PopupAnchor.Top) && rc.Y < bounds.Y ||
+                    edge.HasAllFlags(PopupAnchor.Right) && rc.Right > bounds.Right ||
+                    edge.HasAllFlags(PopupAnchor.Bottom) && rc.Bottom > bounds.Bottom)
                 {
                     return false;
                 }
@@ -147,7 +147,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             // If flipping geometry and anchor is allowed and helps, use the flipped one,
             // otherwise leave it as is
             if (!FitsInBounds(geo, PopupAnchor.HorizontalMask)
-                && constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.FlipX))
+                && constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.FlipX))
             {
                 var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
                 if (FitsInBounds(flipped, PopupAnchor.HorizontalMask))
@@ -155,7 +155,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             }
 
             // If sliding is allowed, try moving the rect into the bounds
-            if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.SlideX))
+            if (constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.SlideX))
             {
                 geo = geo.WithX(Math.Max(geo.X, bounds.X));
                 if (geo.Right > bounds.Right)
@@ -163,7 +163,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             }
             
             // Resize the rect horizontally if allowed.
-            if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.ResizeX))
+            if (constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.ResizeX))
             {
                 var unconstrainedRect = geo;
 
@@ -186,7 +186,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             // If flipping geometry and anchor is allowed and helps, use the flipped one,
             // otherwise leave it as is
             if (!FitsInBounds(geo, PopupAnchor.VerticalMask)
-                && constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.FlipY))
+                && constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.FlipY))
             {
                 var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
                 if (FitsInBounds(flipped, PopupAnchor.VerticalMask))
@@ -194,7 +194,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             }
 
             // If sliding is allowed, try moving the rect into the bounds
-            if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.SlideY))
+            if (constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.SlideY))
             {
                 geo = geo.WithY(Math.Max(geo.Y, bounds.Y));
                 if (geo.Bottom > bounds.Bottom)
@@ -202,7 +202,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             }
 
             // Resize the rect vertically if allowed.
-            if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.ResizeY))
+            if (constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.ResizeY))
             {
                 var unconstrainedRect = geo;
 

+ 6 - 6
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -321,7 +321,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets a value indicating whether <see cref="SelectionMode.AlwaysSelected"/> is set.
         /// </summary>
-        protected bool AlwaysSelected => SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected);
+        protected bool AlwaysSelected => SelectionMode.HasAllFlags(SelectionMode.AlwaysSelected);
 
         /// <inheritdoc/>
         public override void BeginInit()
@@ -501,7 +501,7 @@ namespace Avalonia.Controls.Primitives
 
                 if (ItemCount > 0 &&
                     Match(keymap.SelectAll) &&
-                    SelectionMode.HasFlagCustom(SelectionMode.Multiple))
+                    SelectionMode.HasAllFlags(SelectionMode.Multiple))
                 {
                     Selection.SelectAll();
                     e.Handled = true;
@@ -530,7 +530,7 @@ namespace Avalonia.Controls.Primitives
             else if (change.Property == SelectionModeProperty && _selection is object)
             {
                 var newValue = change.NewValue.GetValueOrDefault<SelectionMode>();
-                _selection.SingleSelect = !newValue.HasFlagCustom(SelectionMode.Multiple);
+                _selection.SingleSelect = !newValue.HasAllFlags(SelectionMode.Multiple);
             }
         }
 
@@ -591,8 +591,8 @@ namespace Avalonia.Controls.Primitives
             }
 
             var mode = SelectionMode;
-            var multi = mode.HasFlagCustom(SelectionMode.Multiple);
-            var toggle = toggleModifier || mode.HasFlagCustom(SelectionMode.Toggle);
+            var multi = mode.HasAllFlags(SelectionMode.Multiple);
+            var toggle = toggleModifier || mode.HasAllFlags(SelectionMode.Toggle);
             var range = multi && rangeModifier;
 
             if (!select)
@@ -869,7 +869,7 @@ namespace Avalonia.Controls.Primitives
         {
             return new InternalSelectionModel
             {
-                SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
+                SingleSelect = !SelectionMode.HasAllFlags(SelectionMode.Multiple),
             };
         }
 

+ 2 - 0
src/Avalonia.Controls/ProgressBar.cs

@@ -138,6 +138,8 @@ namespace Avalonia.Controls
         static ProgressBar()
         {
             ValueProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
+            MinimumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
+            MaximumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
             IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
         }
 

+ 2 - 2
src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs

@@ -53,8 +53,8 @@ namespace Avalonia.Controls
         {
             return _owner.GetElementImpl(
                 index,
-                options.HasFlagCustom(ElementRealizationOptions.ForceCreate),
-                options.HasFlagCustom(ElementRealizationOptions.SuppressAutoRecycle));
+                options.HasAllFlags(ElementRealizationOptions.ForceCreate),
+                options.HasAllFlags(ElementRealizationOptions.SuppressAutoRecycle));
         }
 
         protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);

+ 43 - 7
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@@ -13,8 +13,9 @@ namespace Avalonia.Controls.Selection
     internal class InternalSelectionModel : SelectionModel<object?>
     {
         private IList? _writableSelectedItems;
-        private bool _ignoreModelChanges;
+        private int _ignoreModelChanges;
         private bool _ignoreSelectedItemsChanges;
+        private bool _isResetting;
 
         public InternalSelectionModel()
         {
@@ -130,17 +131,31 @@ namespace Avalonia.Controls.Selection
 
             try
             {
-                _ignoreModelChanges = true;
+                ++_ignoreModelChanges;
 
                 using (BatchUpdate())
                 {
                     Clear();
-                    Add(_writableSelectedItems);
+
+                    for (var i = 0; i < _writableSelectedItems.Count; ++i)
+                    {
+                        var index = IndexOf(Source, _writableSelectedItems[i]);
+
+                        if (index != -1)
+                        {
+                            Select(index);
+                        }
+                        else
+                        {
+                            _writableSelectedItems.RemoveAt(i);
+                            --i;
+                        }
+                    }
                 }
             }
             finally
             {
-                _ignoreModelChanges = false;
+                --_ignoreModelChanges;
             }
         }
 
@@ -162,7 +177,7 @@ namespace Avalonia.Controls.Selection
 
         private void OnSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
         {
-            if (_ignoreModelChanges)
+            if (_ignoreModelChanges > 0)
             {
                 return;
             }
@@ -191,6 +206,27 @@ namespace Avalonia.Controls.Selection
             }
         }
 
+        private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
+        {
+            if (e.Action == NotifyCollectionChangedAction.Reset)
+            {
+                ++_ignoreModelChanges;
+                _isResetting = true;
+            }
+
+            base.OnSourceCollectionChanged(e);
+        }
+
+        protected override void OnSourceCollectionChangeFinished()
+        {
+            base.OnSourceCollectionChangeFinished();
+
+            if (_isResetting)
+            {
+                --_ignoreModelChanges;
+            }
+        }
+
         private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems();
 
         private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
@@ -222,7 +258,7 @@ namespace Avalonia.Controls.Selection
             {
                 using var operation = BatchUpdate();
 
-                _ignoreModelChanges = true;
+                ++_ignoreModelChanges;
 
                 switch (e.Action)
                 {
@@ -244,7 +280,7 @@ namespace Avalonia.Controls.Selection
             }
             finally
             {
-                _ignoreModelChanges = false;
+                --_ignoreModelChanges;
             }
         }
 

+ 4 - 3
src/Avalonia.Controls/TextBox.cs

@@ -492,7 +492,8 @@ namespace Avalonia.Controls
         {
             base.OnLostFocus(e);
 
-            if (ContextMenu == null || !ContextMenu.IsOpen)
+            if ((ContextFlyout == null || !ContextFlyout.IsOpen) &&
+                (ContextMenu == null || !ContextMenu.IsOpen))
             {
                 ClearSelection();
                 RevealPassword = false;
@@ -600,7 +601,7 @@ namespace Avalonia.Controls
             var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
 
             bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
-            bool DetectSelection() => e.KeyModifiers.HasFlagCustom(keymap.SelectionModifiers);
+            bool DetectSelection() => e.KeyModifiers.HasAllFlags(keymap.SelectionModifiers);
 
             if (Match(keymap.SelectAll))
             {
@@ -718,7 +719,7 @@ namespace Avalonia.Controls
             }
             else
             {
-                bool hasWholeWordModifiers = modifiers.HasFlagCustom(keymap.WholeWordTextActionModifiers);
+                bool hasWholeWordModifiers = modifiers.HasAllFlags(keymap.WholeWordTextActionModifiers);
                 switch (e.Key)
                 {
                     case Key.Left:

+ 5 - 5
src/Avalonia.Controls/TreeView.cs

@@ -412,7 +412,7 @@ namespace Avalonia.Controls
                 e.Handled = UpdateSelectionFromEventSource(
                     e.Source,
                     true,
-                    e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift));
+                    e.KeyModifiers.HasAllFlags(KeyModifiers.Shift));
             }
         }
 
@@ -521,8 +521,8 @@ namespace Avalonia.Controls
                     e.Handled = UpdateSelectionFromEventSource(
                         e.Source,
                         true,
-                        e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift),
-                        e.KeyModifiers.HasFlagCustom(KeyModifiers.Control),
+                        e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
+                        e.KeyModifiers.HasAllFlags(KeyModifiers.Control),
                         point.Properties.IsRightButtonPressed);
                 }
             }
@@ -558,8 +558,8 @@ namespace Avalonia.Controls
             }
 
             var mode = SelectionMode;
-            var toggle = toggleModifier || mode.HasFlagCustom(SelectionMode.Toggle);
-            var multi = mode.HasFlagCustom(SelectionMode.Multiple);
+            var toggle = toggleModifier || mode.HasAllFlags(SelectionMode.Toggle);
+            var multi = mode.HasAllFlags(SelectionMode.Multiple);
             var range = multi && rangeModifier && selectedContainer != null;
 
             if (rightButton)

+ 4 - 4
src/Avalonia.Controls/Window.cs

@@ -859,19 +859,19 @@ namespace Avalonia.Controls
             var constraint = clientSize;
             var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
 
-            if (sizeToContent.HasFlagCustom(SizeToContent.Width))
+            if (sizeToContent.HasAllFlags(SizeToContent.Width))
             {
                 constraint = constraint.WithWidth(maxAutoSize.Width);
             }
 
-            if (sizeToContent.HasFlagCustom(SizeToContent.Height))
+            if (sizeToContent.HasAllFlags(SizeToContent.Height))
             {
                 constraint = constraint.WithHeight(maxAutoSize.Height);
             }
 
             var result = base.MeasureOverride(constraint);
 
-            if (!sizeToContent.HasFlagCustom(SizeToContent.Width))
+            if (!sizeToContent.HasAllFlags(SizeToContent.Width))
             {
                 if (!double.IsInfinity(availableSize.Width))
                 {
@@ -883,7 +883,7 @@ namespace Avalonia.Controls
                 }
             }
 
-            if (!sizeToContent.HasFlagCustom(SizeToContent.Height))
+            if (!sizeToContent.HasAllFlags(SizeToContent.Height))
             {
                 if (!double.IsInfinity(availableSize.Height))
                 {

+ 8 - 8
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs

@@ -76,13 +76,13 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
         protected override async Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
         {
             FcitxKeyState state = default;
-            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control))
+            if (args.Modifiers.HasAllFlags(RawInputModifiers.Control))
                 state |= FcitxKeyState.FcitxKeyState_Ctrl;
-            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt))
+            if (args.Modifiers.HasAllFlags(RawInputModifiers.Alt))
                 state |= FcitxKeyState.FcitxKeyState_Alt;
-            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift))
+            if (args.Modifiers.HasAllFlags(RawInputModifiers.Shift))
                 state |= FcitxKeyState.FcitxKeyState_Shift;
-            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta))
+            if (args.Modifiers.HasAllFlags(RawInputModifiers.Meta))
                 state |= FcitxKeyState.FcitxKeyState_Super;
 
             var type = args.Type == RawKeyEventType.KeyDown ?
@@ -126,13 +126,13 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
         {
             var state = (FcitxKeyState)ev.state;
             KeyModifiers mods = default;
-            if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Ctrl))
+            if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Ctrl))
                 mods |= KeyModifiers.Control;
-            if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Alt))
+            if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Alt))
                 mods |= KeyModifiers.Alt;
-            if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Shift))
+            if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Shift))
                 mods |= KeyModifiers.Shift;
-            if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Super))
+            if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Super))
                 mods |= KeyModifiers.Meta;
             FireForward(new X11InputMethodForwardedKey
             {

+ 9 - 9
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs

@@ -33,18 +33,18 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
         {
             var state = (IBusModifierMask)k.state;
             KeyModifiers mods = default;
-            if (state.HasFlagCustom(IBusModifierMask.ControlMask))
+            if (state.HasAllFlags(IBusModifierMask.ControlMask))
                 mods |= KeyModifiers.Control;
-            if (state.HasFlagCustom(IBusModifierMask.Mod1Mask))
+            if (state.HasAllFlags(IBusModifierMask.Mod1Mask))
                 mods |= KeyModifiers.Alt;
-            if (state.HasFlagCustom(IBusModifierMask.ShiftMask))
+            if (state.HasAllFlags(IBusModifierMask.ShiftMask))
                 mods |= KeyModifiers.Shift;
-            if (state.HasFlagCustom(IBusModifierMask.Mod4Mask))
+            if (state.HasAllFlags(IBusModifierMask.Mod4Mask))
                 mods |= KeyModifiers.Meta;
             FireForward(new X11InputMethodForwardedKey
             {
                 KeyVal = (int)k.keyval,
-                Type = state.HasFlagCustom(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown,
+                Type = state.HasAllFlags(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown,
                 Modifiers = mods
             });
         }
@@ -82,13 +82,13 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
         protected override Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
         {
             IBusModifierMask state = default;
-            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control))
+            if (args.Modifiers.HasAllFlags(RawInputModifiers.Control))
                 state |= IBusModifierMask.ControlMask;
-            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt))
+            if (args.Modifiers.HasAllFlags(RawInputModifiers.Alt))
                 state |= IBusModifierMask.Mod1Mask;
-            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift))
+            if (args.Modifiers.HasAllFlags(RawInputModifiers.Shift))
                 state |= IBusModifierMask.ShiftMask;
-            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta))
+            if (args.Modifiers.HasAllFlags(RawInputModifiers.Meta))
                 state |= IBusModifierMask.Mod4Mask;
 
             if (args.Type == RawKeyEventType.KeyUp)

+ 4 - 4
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@@ -223,13 +223,13 @@ namespace Avalonia.FreeDesktop
                             return null;
                         var lst = new List<string>();
                         var mod = item.Gesture;
-                        if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+                        if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control))
                             lst.Add("Control");
-                        if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+                        if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Alt))
                             lst.Add("Alt");
-                        if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+                        if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Shift))
                             lst.Add("Shift");
-                        if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+                        if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
                             lst.Add("Super");
                         lst.Add(item.Gesture.Key.ToString());
                         return new[] { lst.ToArray() };

+ 2 - 2
src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs

@@ -33,11 +33,11 @@ namespace Avalonia.Headless.Vnc
                 {
                     Window?.MouseMove(pt);
                     foreach (var btn in CheckedButtons)
-                        if (_previousButtons.HasFlagCustom(btn) && !buttons.HasFlagCustom(btn))
+                        if (_previousButtons.HasAllFlags(btn) && !buttons.HasAllFlags(btn))
                             Window?.MouseUp(pt, TranslateButton(btn), modifiers);
                     
                     foreach (var btn in CheckedButtons)
-                        if (!_previousButtons.HasFlagCustom(btn) && buttons.HasFlagCustom(btn))
+                        if (!_previousButtons.HasAllFlags(btn) && buttons.HasAllFlags(btn))
                             Window?.MouseDown(pt, TranslateButton(btn), modifiers);
                     _previousButtons = buttons;
                 }, DispatcherPriority.Input);

+ 1 - 1
src/Avalonia.Input/AccessKeyHandler.cs

@@ -177,7 +177,7 @@ namespace Avalonia.Input
         {
             bool menuIsOpen = MainMenu?.IsOpen == true;
 
-            if (e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt) || menuIsOpen)
+            if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) || menuIsOpen)
             {
                 // If any other key is pressed with the Alt key held down, or the main menu is open,
                 // find all controls who have registered that access key.

+ 4 - 4
src/Avalonia.Input/KeyGesture.cs

@@ -115,24 +115,24 @@ namespace Avalonia.Input
                 }
             }
 
-            if (KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+            if (KeyModifiers.HasAllFlags(KeyModifiers.Control))
             {
                 s.Append("Ctrl");
             }
 
-            if (KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+            if (KeyModifiers.HasAllFlags(KeyModifiers.Shift))
             {
                 Plus(s);
                 s.Append("Shift");
             }
 
-            if (KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+            if (KeyModifiers.HasAllFlags(KeyModifiers.Alt))
             {
                 Plus(s);
                 s.Append("Alt");
             }
 
-            if (KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+            if (KeyModifiers.HasAllFlags(KeyModifiers.Meta))
             {
                 Plus(s);
                 s.Append("Cmd");

+ 5 - 5
src/Avalonia.Input/PointerPoint.cs

@@ -31,11 +31,11 @@ namespace Avalonia.Input
         {
             PointerUpdateKind = kind;
 
-            IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton);
-            IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton);
-            IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton);
-            IsXButton1Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton1MouseButton);
-            IsXButton2Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton2MouseButton);
+            IsLeftButtonPressed = modifiers.HasAllFlags(RawInputModifiers.LeftMouseButton);
+            IsMiddleButtonPressed = modifiers.HasAllFlags(RawInputModifiers.MiddleMouseButton);
+            IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton);
+            IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton);
+            IsXButton2Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton2MouseButton);
 
             // The underlying input source might be reporting the previous state,
             // so make sure that we reflect the current state

+ 3 - 3
src/Avalonia.Interactivity/EventRoute.cs

@@ -88,14 +88,14 @@ namespace Avalonia.Interactivity
             }
             else
             {
-                if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel))
+                if (_event.RoutingStrategies.HasAllFlags(RoutingStrategies.Tunnel))
                 {
                     e.Route = RoutingStrategies.Tunnel;
                     RaiseEventImpl(e);
                     _event.InvokeRouteFinished(e);
                 }
 
-                if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble))
+                if (_event.RoutingStrategies.HasAllFlags(RoutingStrategies.Bubble))
                 {
                     e.Route = RoutingStrategies.Bubble;
                     RaiseEventImpl(e);
@@ -159,7 +159,7 @@ namespace Avalonia.Interactivity
 
                 // Raise the event handler.
                 if (entry.Handler is object &&
-                    entry.Routes.HasFlagCustom(e.Route) &&
+                    entry.Routes.HasAllFlags(e.Route) &&
                     (!e.Handled || entry.HandledEventsToo))
                 {
                     if (entry.Adapter is object)

+ 2 - 2
src/Avalonia.Interactivity/Interactive.cs

@@ -155,8 +155,8 @@ namespace Avalonia.Interactivity
             var result = new EventRoute(e);
             var hasClassHandlers = e.HasRaisedSubscriptions;
 
-            if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) ||
-                e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel))
+            if (e.RoutingStrategies.HasAllFlags(RoutingStrategies.Bubble) ||
+                e.RoutingStrategies.HasAllFlags(RoutingStrategies.Tunnel))
             {
                 IInteractive? element = this;
 

+ 2 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -60,4 +60,6 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.SplitView.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.DatePicker.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.TimePicker.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.FlyoutPresenter.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.MenuFlyoutPresenter.xaml?assembly=Avalonia.Themes.Default"/>
 </Styles>

+ 32 - 0
src/Avalonia.Themes.Default/FlyoutPresenter.xaml

@@ -0,0 +1,32 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="FlyoutPresenter">
+    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
+    <Setter Property="VerticalContentAlignment" Value="Stretch" />
+    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
+    <Setter Property="BorderThickness" Value="1" />
+    <Setter Property="Padding" Value="4" />
+    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
+    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Name="LayoutRoot"
+                Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
+                        VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
+            <ContentPresenter Content="{TemplateBinding Content}"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Margin="{TemplateBinding Padding}"
+                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
+                              HorizontalContentAlignment="Stretch"
+                              VerticalContentAlignment="Stretch" />
+          </ScrollViewer>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 29 - 0
src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml

@@ -0,0 +1,29 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="MenuFlyoutPresenter">
+    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
+    <Setter Property="BorderThickness" Value="1" />
+    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
+    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Name="LayoutRoot"
+                Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
+                        VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
+            <ItemsPresenter Name="PART_ItemsPresenter"
+                            Items="{TemplateBinding Items}"
+                            ItemsPanel="{TemplateBinding ItemsPanel}"
+                            ItemTemplate="{TemplateBinding ItemTemplate}"
+                            Margin="{TemplateBinding Padding}"
+                            KeyboardNavigation.TabNavigation="Continue"
+                            Grid.IsSharedSizeScope="True" />
+          </ScrollViewer>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 9 - 1
src/Avalonia.Themes.Default/TextBox.xaml

@@ -3,6 +3,14 @@
     <StreamGeometry x:Key="TextBoxClearButtonData">M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z</StreamGeometry>
     <StreamGeometry x:Key="PasswordBoxRevealButtonData">m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z</StreamGeometry>
     <StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
+
+    <MenuFlyout x:Key="DefaultTextBoxContextFlyout">
+      <MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
+      <MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
+      <MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
+    </MenuFlyout>
+
+    <!-- ContextMenu obsolete, prefer ContextFlyout --> 
     <ContextMenu x:Key="DefaultTextBoxContextMenu" x:Name="TextBoxContextMenu">
       <MenuItem x:Name="TextBoxContextMenuCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
       <MenuItem x:Name="TextBoxContextMenuCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
@@ -17,7 +25,7 @@
     <Setter Property="SelectionBrush" Value="{DynamicResource HighlightBrush}"/>
     <Setter Property="SelectionForegroundBrush" Value="{DynamicResource HighlightForegroundBrush}"/>
     <Setter Property="Padding" Value="4"/>
-    <Setter Property="ContextMenu" Value="{StaticResource DefaultTextBoxContextMenu}" />
+    <Setter Property="ContextFlyout" Value="{StaticResource DefaultTextBoxContextFlyout}" />
     <Setter Property="Template">
       <ControlTemplate>
         <Border Name="border"

+ 5 - 0
src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml

@@ -809,5 +809,10 @@
     <StaticResource x:Key="CalendarDatePickerForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
     <StaticResource x:Key="CalendarDatePickerBorderBrush" ResourceKey="SystemControlForegroundBaseMediumBrush" />
     <Thickness x:Key="CalendarDatePickerBorderThemeThickness">1</Thickness>
+
+    <!-- Resources for FlyoutPresenter.xaml -->
+    <StaticResource x:Key="FlyoutPresenterBackground" ResourceKey="SystemControlBackgroundChromeMediumLowBrush" />
+    <StaticResource x:Key="FlyoutBorderThemeBrush" ResourceKey="SystemControlForegroundChromeHighBrush" />
+
   </Style.Resources>
 </Style>

+ 5 - 0
src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml

@@ -807,5 +807,10 @@
     <StaticResource x:Key="CalendarDatePickerForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
     <StaticResource x:Key="CalendarDatePickerBorderBrush" ResourceKey="SystemControlForegroundBaseMediumBrush" />
     <Thickness x:Key="CalendarDatePickerBorderThemeThickness">1</Thickness>
+
+    <!-- Resources for FlyoutPresenter.xaml -->
+    <StaticResource x:Key="FlyoutPresenterBackground" ResourceKey="SystemControlBackgroundChromeMediumLowBrush" />
+    <StaticResource x:Key="FlyoutBorderThemeBrush" ResourceKey="SystemControlForegroundChromeHighBrush" />
+    
   </Style.Resources>
 </Style>

+ 2 - 0
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@@ -59,4 +59,6 @@
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/SplitView.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/DatePicker.xaml"/>  
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/TimePicker.xaml"/>  
+  <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml"/>
+  <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml"/>
 </Styles>

+ 43 - 0
src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml

@@ -0,0 +1,43 @@
+<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <Styles.Resources>
+    <Thickness x:Key="FlyoutBorderThemeThickness">1</Thickness>
+    <Thickness x:Key="FlyoutBorderThemePadding">0</Thickness>
+  </Styles.Resources>
+  
+  <Style Selector="FlyoutPresenter">
+    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
+    <Setter Property="VerticalContentAlignment" Value="Stretch" />
+    <Setter Property="Background" Value="{DynamicResource FlyoutPresenterBackground}" />
+    <Setter Property="BorderBrush" Value="{DynamicResource FlyoutBorderThemeBrush}" />
+    <Setter Property="BorderThickness" Value="{DynamicResource FlyoutBorderThemeThickness}" />
+    <Setter Property="Padding" Value="{DynamicResource FlyoutContentThemePadding}" />
+    <Setter Property="MinWidth" Value="{DynamicResource FlyoutThemeMinWidth}" />
+    <Setter Property="MaxWidth" Value="{DynamicResource FlyoutThemeMaxWidth}" />
+    <Setter Property="MinHeight" Value="{DynamicResource FlyoutThemeMinHeight}" />
+    <Setter Property="MaxHeight" Value="{DynamicResource FlyoutThemeMaxHeight}" />
+    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
+    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Name="LayoutRoot"
+                Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Padding="{DynamicResource FlyoutBorderThemePadding}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
+                        VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
+            <ContentPresenter Content="{TemplateBinding Content}"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Margin="{TemplateBinding Padding}"
+                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
+                              HorizontalContentAlignment="Stretch"
+                              VerticalContentAlignment="Stretch" />
+          </ScrollViewer>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 35 - 0
src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml

@@ -0,0 +1,35 @@
+<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  
+  <Style Selector="MenuFlyoutPresenter">
+    <Setter Property="Background" Value="{DynamicResource MenuFlyoutPresenterBackground}" />
+    <Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderBrush}" />
+    <Setter Property="BorderThickness" Value="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}" />
+    <Setter Property="Padding" Value="{DynamicResource MenuFlyoutPresenterThemePadding}" />
+    <Setter Property="MaxWidth" Value="{DynamicResource FlyoutThemeMaxWidth}" />
+    <Setter Property="MinHeight" Value="{DynamicResource MenuFlyoutThemeMinHeight}" />
+    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
+    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Name="LayoutRoot"
+                Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Padding="{DynamicResource FlyoutBorderThemePadding}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
+                        VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
+            <ItemsPresenter Name="PART_ItemsPresenter"
+                            Items="{TemplateBinding Items}"
+                            ItemsPanel="{TemplateBinding ItemsPanel}"
+                            ItemTemplate="{TemplateBinding ItemTemplate}"
+                            Margin="{DynamicResource MenuFlyoutScrollerMargin}"
+                            KeyboardNavigation.TabNavigation="Continue"
+                            Grid.IsSharedSizeScope="True" />
+          </ScrollViewer>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 9 - 1
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@@ -14,6 +14,14 @@
     <StreamGeometry x:Key="TextBoxClearButtonData">M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z</StreamGeometry>
     <StreamGeometry x:Key="PasswordBoxRevealButtonData">m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z</StreamGeometry>
     <StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
+
+    <MenuFlyout x:Key="DefaultTextBoxContextFlyout">
+      <MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
+      <MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
+      <MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
+    </MenuFlyout>
+
+    <!-- ContextMenu obsolete, prefer ContextFlyout -->
     <ContextMenu x:Key="DefaultTextBoxContextMenu" x:Name="TextBoxContextMenu">
       <MenuItem x:Name="TextBoxContextMenuCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
       <MenuItem x:Name="TextBoxContextMenuCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
@@ -33,7 +41,7 @@
     <Setter Property="MinWidth" Value="{DynamicResource TextControlThemeMinWidth}" />
     <Setter Property="Padding" Value="{DynamicResource TextControlThemePadding}" />
     <Setter Property="FocusAdorner" Value="{x:Null}" />
-    <Setter Property="ContextMenu" Value="{StaticResource DefaultTextBoxContextMenu}" />
+    <Setter Property="ContextFlyout" Value="{StaticResource DefaultTextBoxContextFlyout}" />
     <Setter Property="Template">
       <ControlTemplate>
         <DataValidationErrors>

+ 2 - 2
src/Avalonia.X11/X11Window.Ime.cs

@@ -96,14 +96,14 @@ namespace Avalonia.X11
 
         void HandleKeyEvent(ref XEvent ev)
         {
-            var index = ev.KeyEvent.state.HasFlagCustom(XModifierMask.ShiftMask);
+            var index = ev.KeyEvent.state.HasAllFlags(XModifierMask.ShiftMask);
 
             // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
             var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
                 
             // Manually switch the Shift index for the keypad,
             // there should be a proper way to do this
-            if (ev.KeyEvent.state.HasFlagCustom(XModifierMask.Mod2Mask)
+            if (ev.KeyEvent.state.HasAllFlags(XModifierMask.Mod2Mask)
                 && key > X11Key.Num_Lock && key <= X11Key.KP_9)
                 key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
             

+ 9 - 9
src/Avalonia.X11/X11Window.cs

@@ -639,23 +639,23 @@ namespace Avalonia.X11
         RawInputModifiers TranslateModifiers(XModifierMask state)
         {
             var rv = default(RawInputModifiers);
-            if (state.HasFlagCustom(XModifierMask.Button1Mask))
+            if (state.HasAllFlags(XModifierMask.Button1Mask))
                 rv |= RawInputModifiers.LeftMouseButton;
-            if (state.HasFlagCustom(XModifierMask.Button2Mask))
+            if (state.HasAllFlags(XModifierMask.Button2Mask))
                 rv |= RawInputModifiers.RightMouseButton;
-            if (state.HasFlagCustom(XModifierMask.Button3Mask))
+            if (state.HasAllFlags(XModifierMask.Button3Mask))
                 rv |= RawInputModifiers.MiddleMouseButton;
-            if (state.HasFlagCustom(XModifierMask.Button4Mask))
+            if (state.HasAllFlags(XModifierMask.Button4Mask))
                 rv |= RawInputModifiers.XButton1MouseButton;
-            if (state.HasFlagCustom(XModifierMask.Button5Mask))
+            if (state.HasAllFlags(XModifierMask.Button5Mask))
                 rv |= RawInputModifiers.XButton2MouseButton;
-            if (state.HasFlagCustom(XModifierMask.ShiftMask))
+            if (state.HasAllFlags(XModifierMask.ShiftMask))
                 rv |= RawInputModifiers.Shift;
-            if (state.HasFlagCustom(XModifierMask.ControlMask))
+            if (state.HasAllFlags(XModifierMask.ControlMask))
                 rv |= RawInputModifiers.Control;
-            if (state.HasFlagCustom(XModifierMask.Mod1Mask))
+            if (state.HasAllFlags(XModifierMask.Mod1Mask))
                 rv |= RawInputModifiers.Alt;
-            if (state.HasFlagCustom(XModifierMask.Mod4Mask))
+            if (state.HasAllFlags(XModifierMask.Mod4Mask))
                 rv |= RawInputModifiers.Meta;
             return rv;
         }

+ 5 - 5
src/Avalonia.X11/XI2Manager.cs

@@ -342,13 +342,13 @@ namespace Avalonia.X11
             Type = ev->evtype;
             Timestamp = (ulong)ev->time.ToInt64();
             var state = (XModifierMask)ev->mods.Effective;
-            if (state.HasFlagCustom(XModifierMask.ShiftMask))
+            if (state.HasAllFlags(XModifierMask.ShiftMask))
                 Modifiers |= RawInputModifiers.Shift;
-            if (state.HasFlagCustom(XModifierMask.ControlMask))
+            if (state.HasAllFlags(XModifierMask.ControlMask))
                 Modifiers |= RawInputModifiers.Control;
-            if (state.HasFlagCustom(XModifierMask.Mod1Mask))
+            if (state.HasAllFlags(XModifierMask.Mod1Mask))
                 Modifiers |= RawInputModifiers.Alt;
-            if (state.HasFlagCustom(XModifierMask.Mod4Mask))
+            if (state.HasAllFlags(XModifierMask.Mod4Mask))
                 Modifiers |= RawInputModifiers.Meta;
 
             Modifiers |= ParseButtonState(ev->buttons.MaskLen, ev->buttons.Mask);
@@ -364,7 +364,7 @@ namespace Avalonia.X11
             if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease)
                 Button = ev->detail;
             Detail = ev->detail;
-            Emulated = ev->flags.HasFlagCustom(XiDeviceEventFlags.XIPointerEmulated);
+            Emulated = ev->flags.HasAllFlags(XiDeviceEventFlags.XIPointerEmulated);
         }
     }
     

+ 1 - 1
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs

@@ -54,7 +54,7 @@ namespace Avalonia.LinuxFramebuffer.Output
         }
 
         public PixelSize Resolution => new PixelSize(Mode.hdisplay, Mode.vdisplay);
-        public bool IsPreferred => Mode.type.HasFlagCustom(DrmModeType.DRM_MODE_TYPE_PREFERRED);
+        public bool IsPreferred => Mode.type.HasAllFlags(DrmModeType.DRM_MODE_TYPE_PREFERRED);
 
         public string Name { get; }
     }

+ 4 - 4
src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs

@@ -147,13 +147,13 @@ namespace Avalonia.Win32.Interop.Wpf
         {
             var state = Keyboard.Modifiers;
             var rv = default(RawInputModifiers);
-            if (state.HasFlagCustom(ModifierKeys.Windows))
+            if (state.HasAllFlags(ModifierKeys.Windows))
                 rv |= RawInputModifiers.Meta;
-            if (state.HasFlagCustom(ModifierKeys.Alt))
+            if (state.HasAllFlags(ModifierKeys.Alt))
                 rv |= RawInputModifiers.Alt;
-            if (state.HasFlagCustom(ModifierKeys.Control))
+            if (state.HasAllFlags(ModifierKeys.Control))
                 rv |= RawInputModifiers.Control;
-            if (state.HasFlagCustom(ModifierKeys.Shift))
+            if (state.HasAllFlags(ModifierKeys.Shift))
                 rv |= RawInputModifiers.Shift;
             if (e != null)
             {

+ 3 - 3
src/Windows/Avalonia.Win32/DataObject.cs

@@ -181,7 +181,7 @@ namespace Avalonia.Win32
                 ole.GetData(ref format, out medium);
                 return;
             }
-            if(!format.tymed.HasFlagCustom(TYMED.TYMED_HGLOBAL))
+            if(!format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL))
                 Marshal.ThrowExceptionForHR(DV_E_TYMED);
 
             if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
@@ -205,7 +205,7 @@ namespace Avalonia.Win32
                 return;
             }
 
-            if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasFlagCustom(TYMED.TYMED_HGLOBAL))
+            if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL))
                 Marshal.ThrowExceptionForHR(DV_E_TYMED);
 
             if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
@@ -228,7 +228,7 @@ namespace Avalonia.Win32
                 return ole.QueryGetData(ref format);
             if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
                 return DV_E_DVASPECT;
-            if (!format.tymed.HasFlagCustom(TYMED.TYMED_HGLOBAL))
+            if (!format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL))
                 return DV_E_TYMED;
 
             string dataFormat = ClipboardFormats.GetFormat(format.cfFormat);

+ 12 - 12
src/Windows/Avalonia.Win32/OleDropTarget.cs

@@ -24,11 +24,11 @@ namespace Avalonia.Win32
         public static DropEffect ConvertDropEffect(DragDropEffects operation)
         {
             DropEffect result = DropEffect.None;
-            if (operation.HasFlagCustom(DragDropEffects.Copy))
+            if (operation.HasAllFlags(DragDropEffects.Copy))
                 result |= DropEffect.Copy;
-            if (operation.HasFlagCustom(DragDropEffects.Move))
+            if (operation.HasAllFlags(DragDropEffects.Move))
                 result |= DropEffect.Move;
-            if (operation.HasFlagCustom(DragDropEffects.Link))
+            if (operation.HasAllFlags(DragDropEffects.Link))
                 result |= DropEffect.Link;
             return result;
         }
@@ -36,11 +36,11 @@ namespace Avalonia.Win32
         public static DragDropEffects ConvertDropEffect(DropEffect effect)
         {
             DragDropEffects result = DragDropEffects.None;
-            if (effect.HasFlagCustom(DropEffect.Copy))
+            if (effect.HasAllFlags(DropEffect.Copy))
                 result |= DragDropEffects.Copy;
-            if (effect.HasFlagCustom(DropEffect.Move))
+            if (effect.HasAllFlags(DropEffect.Move))
                 result |= DragDropEffects.Move;
-            if (effect.HasFlagCustom(DropEffect.Link))
+            if (effect.HasAllFlags(DropEffect.Link))
                 result |= DragDropEffects.Link;
             return result;
         }
@@ -50,17 +50,17 @@ namespace Avalonia.Win32
             var modifiers = RawInputModifiers.None;
             var state = (UnmanagedMethods.ModifierKeys)grfKeyState;
 
-            if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
+            if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
                 modifiers |= RawInputModifiers.LeftMouseButton;
-            if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
+            if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
                 modifiers |= RawInputModifiers.MiddleMouseButton;
-            if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
+            if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
                 modifiers |= RawInputModifiers.RightMouseButton;
-            if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_SHIFT))
+            if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_SHIFT))
                 modifiers |= RawInputModifiers.Shift;
-            if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_CONTROL))
+            if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_CONTROL))
                 modifiers |= RawInputModifiers.Control;
-            if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_ALT))
+            if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_ALT))
                 modifiers |= RawInputModifiers.Alt;
             return modifiers;
         }

+ 7 - 7
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@@ -310,9 +310,9 @@ namespace Avalonia.Win32
                             {
                                 Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
                                     _owner,
-                                    touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ?
+                                    touchInput.Flags.HasAllFlags(TouchInputFlags.TOUCHEVENTF_UP) ?
                                         RawPointerEventType.TouchEnd :
-                                        touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ?
+                                        touchInput.Flags.HasAllFlags(TouchInputFlags.TOUCHEVENTF_DOWN) ?
                                             RawPointerEventType.TouchBegin :
                                             RawPointerEventType.TouchUpdate,
                                     PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
@@ -521,27 +521,27 @@ namespace Avalonia.Win32
             var keys = (ModifierKeys)ToInt32(wParam);
             var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
 
-            if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON))
+            if (keys.HasAllFlags(ModifierKeys.MK_LBUTTON))
             {
                 modifiers |= RawInputModifiers.LeftMouseButton;
             }
 
-            if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON))
+            if (keys.HasAllFlags(ModifierKeys.MK_RBUTTON))
             {
                 modifiers |= RawInputModifiers.RightMouseButton;
             }
 
-            if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON))
+            if (keys.HasAllFlags(ModifierKeys.MK_MBUTTON))
             {
                 modifiers |= RawInputModifiers.MiddleMouseButton;
             }
 
-            if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1))
+            if (keys.HasAllFlags(ModifierKeys.MK_XBUTTON1))
             {
                 modifiers |= RawInputModifiers.XButton1MouseButton;
             }
 
-            if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2))
+            if (keys.HasAllFlags(ModifierKeys.MK_XBUTTON2))
             {
                 modifiers |= RawInputModifiers.XButton2MouseButton;
             }

+ 2 - 2
src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs

@@ -23,13 +23,13 @@ namespace Avalonia.Win32
             AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0);
 
             var borderThickness = new RECT();
-            if (GetStyle().HasFlagCustom(WindowStyles.WS_THICKFRAME))
+            if (GetStyle().HasAllFlags(WindowStyles.WS_THICKFRAME))
             {
                 AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle()), false, 0);
                 borderThickness.left *= -1;
                 borderThickness.top *= -1;
             }
-            else if (GetStyle().HasFlagCustom(WindowStyles.WS_BORDER))
+            else if (GetStyle().HasAllFlags(WindowStyles.WS_BORDER))
             {
                 borderThickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 };
             }

+ 8 - 6
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -580,7 +580,9 @@ namespace Avalonia.Win32
 
         public void SetParent(IWindowImpl parent)
         {
-            var parentHwnd = ((WindowImpl)parent)?._hwnd ?? IntPtr.Zero;
+            _parent = (WindowImpl)parent;
+            
+            var parentHwnd = _parent?._hwnd ?? IntPtr.Zero;
 
             if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar)
             {
@@ -845,7 +847,7 @@ namespace Avalonia.Win32
             borderCaptionThickness.left *= -1;
             borderCaptionThickness.top *= -1;
 
-            bool wantsTitleBar = _extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1;
+            bool wantsTitleBar = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1;
 
             if (!wantsTitleBar)
             {
@@ -862,7 +864,7 @@ namespace Avalonia.Win32
                 borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling);                
             }
 
-            margins.cyTopHeight = _extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1;
+            margins.cyTopHeight = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1;
 
             if (WindowState == WindowState.Maximized)
             {
@@ -916,8 +918,8 @@ namespace Avalonia.Win32
                 Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling));
             }
 
-            if(!_isClientAreaExtended || (_extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.SystemChrome) &&
-                !_extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.PreferSystemChrome)))
+            if(!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) &&
+                !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome)))
             {
                 EnableCloseButton(_hwnd);
             }
@@ -1283,7 +1285,7 @@ namespace Avalonia.Win32
         public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
         
         /// <inheritdoc/>
-        public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.PreferSystemChrome);
+        public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome);
 
         /// <inheritdoc/>
         public Thickness ExtendedMargins => _extendedMargins;

+ 27 - 6
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@@ -269,6 +269,19 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(0, command.SubscriptionCount);
         }
 
+        [Fact]
+        public void Button_Invokes_CanExecute_When_CommandParameter_Changed()
+        {
+            var command = new TestCommand(p => p is bool value && value);
+            var target = new Button { Command = command };
+
+            target.CommandParameter = true;
+            Assert.True(target.IsEffectivelyEnabled);
+
+            target.CommandParameter = false;
+            Assert.False(target.IsEffectivelyEnabled);
+        }
+
         private class TestButton : Button, IRenderRoot
         {
             public TestButton()
@@ -324,12 +337,22 @@ namespace Avalonia.Controls.UnitTests
 
         private class TestCommand : ICommand
         {
+            private readonly Func<object, bool> _canExecute;
+            private readonly Action<object> _execute;
             private EventHandler _canExecuteChanged;
-            private bool _enabled;
+            private bool _enabled = true;
 
-            public TestCommand(bool enabled)
+            public TestCommand(bool enabled = true)
             {
                 _enabled = enabled;
+                _canExecute = _ => _enabled;
+                _execute = _ => { };
+            }
+
+            public TestCommand(Func<object, bool> canExecute, Action<object> execute = null)
+            {
+                _canExecute = canExecute;
+                _execute = execute ?? (_ => { });
             }
 
             public bool IsEnabled
@@ -353,11 +376,9 @@ namespace Avalonia.Controls.UnitTests
                 remove { _canExecuteChanged -= value; --SubscriptionCount; }
             }
 
-            public bool CanExecute(object parameter) => _enabled;
+            public bool CanExecute(object parameter) => _canExecute(parameter);
 
-            public void Execute(object parameter)
-            {
-            }
+            public void Execute(object parameter) => _execute(parameter);
         }
     }
 }

+ 325 - 0
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@@ -0,0 +1,325 @@
+using System;
+using System.Linq;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class FlyoutTests
+    {
+        [Fact]
+        public void Opening_Raises_Single_Opening_Event()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var window = PreparedWindow();
+                window.Show();
+
+                int tracker = 0;
+                Flyout f = new Flyout();
+                f.Opening += (s, e) =>
+                {
+                    tracker++;
+                };
+                f.ShowAt(window);
+
+                Assert.Equal(1, tracker);
+            }
+        }
+
+        [Fact]
+        public void Opening_Raises_Single_Opened_Event()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var window = PreparedWindow();
+                window.Show();
+
+                int tracker = 0;
+                Flyout f = new Flyout();
+                f.Opened += (s, e) =>
+                {
+                    tracker++;
+                };
+                f.ShowAt(window);
+
+                Assert.Equal(1, tracker);
+            }
+        }
+
+        [Fact]
+        public void Closing_Raises_Single_Closing_Event()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var window = PreparedWindow();
+                window.Show();
+
+                int tracker = 0;
+                Flyout f = new Flyout();
+                f.Closing += (s, e) =>
+                {
+                    tracker++;
+                };
+                f.ShowAt(window);
+                f.Hide();
+
+                Assert.Equal(1, tracker);
+            }
+        }
+
+        [Fact]
+        public void Closing_Raises_Single_Closed_Event()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var window = PreparedWindow();
+                window.Show();
+
+                int tracker = 0;
+                Flyout f = new Flyout();
+                f.Closed += (s, e) =>
+                {
+                    tracker++;
+                };
+                f.ShowAt(window);
+                f.Hide();
+
+                Assert.Equal(1, tracker);
+            }
+        }
+
+        [Fact]
+        public void Cancel_Closing_Keeps_Flyout_Open()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var window = PreparedWindow();
+                window.Show();
+
+                int tracker = 0;
+                Flyout f = new Flyout();
+                f.Closing += (s, e) =>
+                {
+                    e.Cancel = true;
+                };
+                f.ShowAt(window);
+                f.Hide();
+
+                Assert.True(f.IsOpen);
+            }
+        }
+
+        [Fact]
+        public void Flyout_Has_Uncancellable_Close_Before_Showing_On_A_Different_Target()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var window = PreparedWindow();
+                Button target1 = new Button();
+                Button target2 = new Button();
+
+                window.Content = new StackPanel
+                {
+                    Children =
+                    {
+                        target1,
+                        target2
+                    }
+                };
+                window.Show();
+
+                bool closingFired = false;
+                bool closedFired = false;
+                Flyout f = new Flyout();
+                f.Closing += (s, e) =>
+                {
+                    closingFired = true; //This shouldn't happen
+                };
+                f.Closed += (s, e) =>
+                {
+                    closedFired = true;
+                };
+
+                f.ShowAt(target1);
+
+                f.ShowAt(target2);
+
+                Assert.False(closingFired);
+                Assert.True(closedFired);
+            }
+        }
+
+        [Fact]
+        public void ShowMode_Standard_Attemps_Focus_Flyout_Content()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var window = PreparedWindow();
+
+                var flyoutTextBox = new TextBox();
+                var button = new Button
+                {
+                    Flyout = new Flyout
+                    {
+                        ShowMode = FlyoutShowMode.Standard,
+                        Content = new Panel
+                        {
+                            Children =
+                            {
+                                flyoutTextBox
+                            }
+                        }
+                    }
+                };
+
+                window.Content = button;
+                window.Show();
+
+                button.Focus();
+                Assert.True(FocusManager.Instance?.Current == button);
+                button.Flyout.ShowAt(button);
+                Assert.False(button.IsFocused);
+                Assert.True(FocusManager.Instance?.Current == flyoutTextBox);
+            }
+        }
+
+        [Fact]
+        public void ShowMode_Transient_Does_Not_Move_Focus_From_Target()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var window = PreparedWindow();
+
+                var flyoutTextBox = new TextBox();
+                var button = new Button
+                {
+                    Flyout = new Flyout
+                    {
+                        ShowMode = FlyoutShowMode.Transient,
+                        Content = new Panel
+                        {
+                            Children =
+                            {
+                                flyoutTextBox
+                            }
+                        }
+                    },
+                    Content = "Test"
+                };
+
+                window.Content = button;
+                window.Show();
+
+                FocusManager.Instance?.Focus(button);
+                Assert.True(FocusManager.Instance?.Current == button);
+                button.Flyout.ShowAt(button);
+                Assert.True(FocusManager.Instance?.Current == button);
+            }
+        }
+
+        [Fact]
+        public void ContextFlyout_Can_Be_Set_In_Styles()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style Selector='TextBlock'>
+            <Setter Property='ContextFlyout'>
+                <MenuFlyout>
+                    <MenuItem>Foo</MenuItem>
+                </MenuFlyout>
+            </Setter>
+        </Style>
+	</Window.Styles>
+
+    <StackPanel>
+        <TextBlock Name='target1'/>
+        <TextBlock Name='target2'/>
+    </StackPanel>
+</Window>";
+
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var target1 = window.Find<TextBlock>("target1");
+                var target2 = window.Find<TextBlock>("target2");
+                var mouse = new MouseTestHelper();
+
+                Assert.NotNull(target1.ContextFlyout);
+                Assert.NotNull(target2.ContextFlyout);
+                Assert.Same(target1.ContextFlyout, target2.ContextFlyout);
+
+                window.Show();
+
+                var menu = target1.ContextFlyout;
+                mouse.Click(target1, MouseButton.Right);
+                Assert.True(menu.IsOpen);
+                mouse.Click(target2, MouseButton.Right);
+                Assert.True(menu.IsOpen);
+            }
+        }
+
+        [Fact]
+        public void Setting_FlyoutPresenterClasses_Sets_Classes_On_FlyoutPresenter()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style Selector='FlyoutPresenter.TestClass'>
+            <Setter Property='Background' Value='Red' />
+        </Style>
+	</Window.Styles>
+</Window>";
+
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var flyoutPanel = new Panel();
+                var button = new Button
+                {
+                    Content = "Test",
+                    Flyout = new Flyout
+                    {
+                        Content = flyoutPanel
+                    }
+                };
+                window.Content = button;
+                window.Show();
+
+                (button.Flyout as Flyout).FlyoutPresenterClasses.Add("TestClass");
+
+                button.Flyout.ShowAt(button);
+
+                var presenter = flyoutPanel.GetVisualAncestors().OfType<FlyoutPresenter>().FirstOrDefault();
+                Assert.NotNull(presenter);
+                Assert.True((presenter.Background as ISolidColorBrush).Color == Colors.Red);
+            }
+        }
+
+        private IDisposable CreateServicesWithFocus()
+        {
+            return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
+                new MockWindowingPlatform(null,
+                    x =>
+                    {
+                        return MockWindowingPlatform.CreatePopupMock(x).Object;
+                    }),
+                    focusManager: new FocusManager(),
+                    keyboardDevice: () => new KeyboardDevice()));
+        }
+
+        private Window PreparedWindow(object content = null)
+        {
+            var w = new Window { Content = content };
+            w.ApplyTemplate();
+            return w;
+        }
+    }
+}

+ 7 - 0
tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs

@@ -97,6 +97,7 @@ namespace Avalonia.Controls.UnitTests.Selection
             target.WritableSelectedItems.Clear();
 
             Assert.Empty(target.SelectedIndexes);
+            Assert.Empty(target.WritableSelectedItems);
         }
 
         [Fact]
@@ -123,6 +124,7 @@ namespace Avalonia.Controls.UnitTests.Selection
             target.WritableSelectedItems = null;
 
             Assert.Empty(target.SelectedIndexes);
+            Assert.Empty(target.WritableSelectedItems);
         }
 
         [Fact]
@@ -182,6 +184,7 @@ namespace Avalonia.Controls.UnitTests.Selection
             target.Source = items;
 
             Assert.Equal(1, target.SelectedIndex);
+            Assert.Equal(new[] { "bar" }, target.WritableSelectedItems);
         }
 
         [Fact]
@@ -203,6 +206,7 @@ namespace Avalonia.Controls.UnitTests.Selection
             items.Reset(new[] { "baz", "foo", "bar" });
 
             Assert.Equal(2, target.SelectedIndex);
+            Assert.Equal(new[] { "bar" }, target.WritableSelectedItems);
         }
 
         [Fact]
@@ -227,6 +231,7 @@ namespace Avalonia.Controls.UnitTests.Selection
 
             Assert.Equal(-1, target.SelectedIndex);
             Assert.Equal(null, target.SelectedItem);
+            Assert.Empty(target.WritableSelectedItems);
 
             Assert.Contains(nameof(target.SelectedIndex), changed);
             Assert.Contains(nameof(target.SelectedItem), changed);
@@ -246,6 +251,7 @@ namespace Avalonia.Controls.UnitTests.Selection
 
             Assert.Equal("foo", target.SelectedItem);
             Assert.Equal(1, target.SelectedIndex);
+            Assert.Equal(new[] { "foo" }, target.WritableSelectedItems);
         }
 
         [Fact]
@@ -257,6 +263,7 @@ namespace Avalonia.Controls.UnitTests.Selection
             target.Source = new[] { "baz", "foo", "bar" };
 
             Assert.Equal(2, target.SelectedIndex);
+            Assert.Equal(new[] { "bar" }, target.WritableSelectedItems);
         }
 
         private static InternalSelectionModel CreateTarget(

+ 39 - 1
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
 using Avalonia.Controls.Presenters;
@@ -56,7 +57,44 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Equal("123", target1.SelectedText);
             }
         }
-        
+
+        [Fact]
+        public void Opening_Context_Flyout_Does_not_Lose_Selection()
+        {
+            using (UnitTestApplication.Start(FocusServices))
+            {
+                var target1 = new TextBox
+                {
+                    Template = CreateTemplate(),
+                    Text = "1234",
+                    ContextFlyout = new MenuFlyout
+                    {
+                        Items = new List<MenuItem>
+                        {
+                            new MenuItem { Header = "Item 1" },
+                            new MenuItem {Header = "Item 2" },
+                            new MenuItem {Header = "Item 3" }
+                        }
+                    }
+                };
+                              
+
+                target1.ApplyTemplate();
+
+                var root = new TestRoot() { Child = target1 };
+
+                target1.SelectionStart = 0;
+                target1.SelectionEnd = 3;
+
+                target1.Focus();
+                Assert.True(target1.IsFocused);
+
+                target1.ContextFlyout.ShowAt(target1);
+
+                Assert.Equal("123", target1.SelectedText);
+            }
+        }
+
         [Fact]
         public void DefaultBindingMode_Should_Be_TwoWay()
         {