diff --git a/WinUIGallery/ControlPages/TabViewPage.xaml b/WinUIGallery/ControlPages/TabViewPage.xaml index dbd282d22..cd039998d 100644 --- a/WinUIGallery/ControlPages/TabViewPage.xaml +++ b/WinUIGallery/ControlPages/TabViewPage.xaml @@ -1,4 +1,4 @@ - + + + + @@ -25,19 +29,19 @@ - + - + - + @@ -79,7 +83,7 @@ - + @@ -172,19 +176,19 @@ - + - + - + @@ -214,19 +218,19 @@ - + - + - + @@ -259,17 +263,17 @@ - + - + - + diff --git a/WinUIGallery/ControlPages/TabViewPage.xaml.cs b/WinUIGallery/ControlPages/TabViewPage.xaml.cs index 3d95c3074..07da43467 100644 --- a/WinUIGallery/ControlPages/TabViewPage.xaml.cs +++ b/WinUIGallery/ControlPages/TabViewPage.xaml.cs @@ -1,260 +1,268 @@ -using System; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Input; -using WinUIGallery.SamplePages; -using WinUIGallery.Helper; -using Windows.ApplicationModel.Core; -using Microsoft.UI.Xaml; -using Microsoft.UI.Windowing; -using Microsoft.UI.Dispatching; -using WinUIGallery.TabViewPages; -using System.Collections.ObjectModel; - -namespace WinUIGallery.ControlPages -{ - public class MyData - { - public string DataHeader { get; set; } - public Microsoft.UI.Xaml.Controls.IconSource DataIconSource { get; set; } - public object DataContent { get; set; } - } - - public sealed partial class TabViewPage : Page - { - ObservableCollection myDatas; - - public TabViewPage() - { - this.InitializeComponent(); - - // Launching isn't supported yet on Desktop - // Blocked on Task 27517663: DCPP Preview 2 Bug: Dragging in TabView windowing sample causes app to crash - //this.LaunchExample.Visibility = Visibility.Collapsed; - - InitializeDataBindingSampleData(); - } - -#region SharedTabViewLogic - private void TabView_Loaded(object sender, RoutedEventArgs e) - { - for (int i = 0; i < 3; i++) - { - (sender as TabView).TabItems.Add(CreateNewTab(i)); - } - } - - private void TabView_AddButtonClick(TabView sender, object args) - { - sender.TabItems.Add(CreateNewTab(sender.TabItems.Count)); - } - - private void TabView_TabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) - { - sender.TabItems.Remove(args.Tab); - } - - private TabViewItem CreateNewTab(int index) - { - TabViewItem newItem = new TabViewItem - { - Header = $"Document {index}", - IconSource = new Microsoft.UI.Xaml.Controls.SymbolIconSource() { Symbol = Symbol.Document } - }; - - // The content of the tab is often a frame that contains a page, though it could be any UIElement. - Frame frame = new Frame(); - - switch (index % 3) - { - case 0: - frame.Navigate(typeof(SamplePage1)); - break; - case 1: - frame.Navigate(typeof(SamplePage2)); - break; - case 2: - frame.Navigate(typeof(SamplePage3)); - break; - } - - newItem.Content = frame; - - return newItem; - } -#endregion - -#region ItemsSourceSample - private void InitializeDataBindingSampleData() - { - myDatas = new ObservableCollection(); - - for (int index = 0; index < 3; index++) - { - myDatas.Add(CreateNewMyData(index)); - } - } - - private MyData CreateNewMyData(int index) - { - var newData = new MyData - { - DataHeader = $"MyData Doc {index}", - DataIconSource = new Microsoft.UI.Xaml.Controls.SymbolIconSource() { Symbol = Symbol.Placeholder } - }; - - Frame frame = new Frame(); - - switch (index % 3) - { - case 0: - frame.Navigate(typeof(SamplePage1)); - break; - case 1: - frame.Navigate(typeof(SamplePage2)); - break; - case 2: - frame.Navigate(typeof(SamplePage3)); - break; - } - - newData.DataContent = frame; - - return newData; - } - - private void TabViewItemsSourceSample_AddTabButtonClick(TabView sender, object args) - { - // Add a new MyData item to the collection. TabView automatically generates a TabViewItem. - myDatas.Add(CreateNewMyData(myDatas.Count)); - } - - private void TabViewItemsSourceSample_TabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) - { - // Remove the requested MyData object from the collection. - myDatas.Remove(args.Item as MyData); - } -#endregion - -#region KeyboardAcceleratorSample - private void NewTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) - { - var senderTabView = args.Element as TabView; - senderTabView.TabItems.Add(CreateNewTab(senderTabView.TabItems.Count)); - - args.Handled = true; - } - - private void CloseSelectedTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) - { - var InvokedTabView = (args.Element as TabView); - - // Only close the selected tab if it is closeable - if (((TabViewItem)InvokedTabView.SelectedItem).IsClosable) - { - InvokedTabView.TabItems.Remove(InvokedTabView.SelectedItem); - } - - args.Handled = true; - } - - private void NavigateToNumberedTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) - { - var InvokedTabView = (args.Element as TabView); - - int tabToSelect = 0; - - switch (sender.Key) - { - case Windows.System.VirtualKey.Number1: - tabToSelect = 0; - break; - case Windows.System.VirtualKey.Number2: - tabToSelect = 1; - break; - case Windows.System.VirtualKey.Number3: - tabToSelect = 2; - break; - case Windows.System.VirtualKey.Number4: - tabToSelect = 3; - break; - case Windows.System.VirtualKey.Number5: - tabToSelect = 4; - break; - case Windows.System.VirtualKey.Number6: - tabToSelect = 5; - break; - case Windows.System.VirtualKey.Number7: - tabToSelect = 6; - break; - case Windows.System.VirtualKey.Number8: - tabToSelect = 7; - break; - case Windows.System.VirtualKey.Number9: - // Select the last tab - tabToSelect = InvokedTabView.TabItems.Count - 1; - break; - } - - // Only select the tab if it is in the list - if (tabToSelect < InvokedTabView.TabItems.Count) - { - InvokedTabView.SelectedIndex = tabToSelect; - } - - args.Handled = true; - } -#endregion - - private void TabWidthBehaviorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - string widthModeString = (e.AddedItems[0] as ComboBoxItem).Content.ToString(); - TabViewWidthMode widthMode = TabViewWidthMode.Equal; - switch (widthModeString) - { - case "Equal": - widthMode = TabViewWidthMode.Equal; - break; - case "SizeToContent": - widthMode = TabViewWidthMode.SizeToContent; - break; - case "Compact": - widthMode = TabViewWidthMode.Compact; - break; - } - TabView3.TabWidthMode = widthMode; - } - - private void TabCloseButtonOverlayModeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - string overlayModeString = (e.AddedItems[0] as ComboBoxItem).Content.ToString(); - TabViewCloseButtonOverlayMode overlayMode = TabViewCloseButtonOverlayMode.Auto; - switch (overlayModeString) - { - case "Auto": - overlayMode = TabViewCloseButtonOverlayMode.Auto; - break; - case "OnHover": - overlayMode = TabViewCloseButtonOverlayMode.OnPointerOver; - break; - case "Always": - overlayMode = TabViewCloseButtonOverlayMode.Always; - break; - } - TabView4.CloseButtonOverlayMode = overlayMode; - } - - private void TabViewWindowingButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) - { - var tabViewSample = new TabViewWindowingSamplePage(); - - var newWindow = WindowHelper.CreateWindow(); - newWindow.ExtendsContentIntoTitleBar = true; - newWindow.Content = tabViewSample; - newWindow.AppWindow.SetIcon("Assets/Tiles/GalleryIcon.ico"); - tabViewSample.LoadDemoData(); - tabViewSample.SetupWindowMinSize(newWindow); - - newWindow.Activate(); - } - } -} +using System; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using WinUIGallery.SamplePages; +using WinUIGallery.Helper; +using Windows.ApplicationModel.Core; +using Microsoft.UI.Xaml; +using Microsoft.UI.Windowing; +using Microsoft.UI.Dispatching; +using WinUIGallery.TabViewPages; +using System.Collections.ObjectModel; +using Microsoft.UI.Xaml.Media; +using System.Collections; + +namespace WinUIGallery.ControlPages +{ + public class MyData + { + public string DataHeader { get; set; } + public Microsoft.UI.Xaml.Controls.IconSource DataIconSource { get; set; } + public object DataContent { get; set; } + } + + public sealed partial class TabViewPage : Page + { + ObservableCollection myDatas; + + public TabViewPage() + { + this.InitializeComponent(); + + // Launching isn't supported yet on Desktop + // Blocked on Task 27517663: DCPP Preview 2 Bug: Dragging in TabView windowing sample causes app to crash + //this.LaunchExample.Visibility = Visibility.Collapsed; + + InitializeDataBindingSampleData(); + } + +#region SharedTabViewLogic + private void TabView_Loaded(object sender, RoutedEventArgs e) + { + for (int i = 0; i < 3; i++) + { + (sender as TabView).TabItems.Add(CreateNewTab(i)); + } + } + + private void TabView_AddButtonClick(TabView sender, object args) + { + sender.TabItems.Add(CreateNewTab(sender.TabItems.Count)); + } + + private void TabView_TabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) + { + sender.TabItems.Remove(args.Tab); + } + + private TabViewItem CreateNewTab(int index) + { + TabViewItem newItem = new TabViewItem + { + Header = $"Document {index}", + IconSource = new Microsoft.UI.Xaml.Controls.SymbolIconSource() { Symbol = Symbol.Document }, + ContextFlyout = TabViewContextMenu + }; + + // The content of the tab is often a frame that contains a page, though it could be any UIElement. + Frame frame = new Frame(); + + switch (index % 3) + { + case 0: + frame.Navigate(typeof(SamplePage1)); + break; + case 1: + frame.Navigate(typeof(SamplePage2)); + break; + case 2: + frame.Navigate(typeof(SamplePage3)); + break; + } + + newItem.Content = frame; + + return newItem; + } +#endregion + +#region ItemsSourceSample + private void InitializeDataBindingSampleData() + { + myDatas = new ObservableCollection(); + + for (int index = 0; index < 3; index++) + { + myDatas.Add(CreateNewMyData(index)); + } + } + + private MyData CreateNewMyData(int index) + { + var newData = new MyData + { + DataHeader = $"MyData Doc {index}", + DataIconSource = new Microsoft.UI.Xaml.Controls.SymbolIconSource() { Symbol = Symbol.Placeholder } + }; + + Frame frame = new Frame(); + + switch (index % 3) + { + case 0: + frame.Navigate(typeof(SamplePage1)); + break; + case 1: + frame.Navigate(typeof(SamplePage2)); + break; + case 2: + frame.Navigate(typeof(SamplePage3)); + break; + } + + newData.DataContent = frame; + + return newData; + } + + private void TabViewItemsSourceSample_AddTabButtonClick(TabView sender, object args) + { + // Add a new MyData item to the collection. TabView automatically generates a TabViewItem. + myDatas.Add(CreateNewMyData(myDatas.Count)); + } + + private void TabViewItemsSourceSample_TabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) + { + // Remove the requested MyData object from the collection. + myDatas.Remove(args.Item as MyData); + } +#endregion + +#region KeyboardAcceleratorSample + private void NewTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) + { + var senderTabView = args.Element as TabView; + senderTabView.TabItems.Add(CreateNewTab(senderTabView.TabItems.Count)); + + args.Handled = true; + } + + private void CloseSelectedTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) + { + var InvokedTabView = (args.Element as TabView); + + // Only close the selected tab if it is closeable + if (((TabViewItem)InvokedTabView.SelectedItem).IsClosable) + { + InvokedTabView.TabItems.Remove(InvokedTabView.SelectedItem); + } + + args.Handled = true; + } + + private void NavigateToNumberedTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) + { + var InvokedTabView = (args.Element as TabView); + + int tabToSelect = 0; + + switch (sender.Key) + { + case Windows.System.VirtualKey.Number1: + tabToSelect = 0; + break; + case Windows.System.VirtualKey.Number2: + tabToSelect = 1; + break; + case Windows.System.VirtualKey.Number3: + tabToSelect = 2; + break; + case Windows.System.VirtualKey.Number4: + tabToSelect = 3; + break; + case Windows.System.VirtualKey.Number5: + tabToSelect = 4; + break; + case Windows.System.VirtualKey.Number6: + tabToSelect = 5; + break; + case Windows.System.VirtualKey.Number7: + tabToSelect = 6; + break; + case Windows.System.VirtualKey.Number8: + tabToSelect = 7; + break; + case Windows.System.VirtualKey.Number9: + // Select the last tab + tabToSelect = InvokedTabView.TabItems.Count - 1; + break; + } + + // Only select the tab if it is in the list + if (tabToSelect < InvokedTabView.TabItems.Count) + { + InvokedTabView.SelectedIndex = tabToSelect; + } + + args.Handled = true; + } +#endregion + + private void TabWidthBehaviorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + string widthModeString = (e.AddedItems[0] as ComboBoxItem).Content.ToString(); + TabViewWidthMode widthMode = TabViewWidthMode.Equal; + switch (widthModeString) + { + case "Equal": + widthMode = TabViewWidthMode.Equal; + break; + case "SizeToContent": + widthMode = TabViewWidthMode.SizeToContent; + break; + case "Compact": + widthMode = TabViewWidthMode.Compact; + break; + } + TabView3.TabWidthMode = widthMode; + } + + private void TabCloseButtonOverlayModeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + string overlayModeString = (e.AddedItems[0] as ComboBoxItem).Content.ToString(); + TabViewCloseButtonOverlayMode overlayMode = TabViewCloseButtonOverlayMode.Auto; + switch (overlayModeString) + { + case "Auto": + overlayMode = TabViewCloseButtonOverlayMode.Auto; + break; + case "OnHover": + overlayMode = TabViewCloseButtonOverlayMode.OnPointerOver; + break; + case "Always": + overlayMode = TabViewCloseButtonOverlayMode.Always; + break; + } + TabView4.CloseButtonOverlayMode = overlayMode; + } + + private void TabViewWindowingButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var tabViewSample = new TabViewWindowingSamplePage(); + + var newWindow = WindowHelper.CreateWindow(); + newWindow.ExtendsContentIntoTitleBar = true; + newWindow.Content = tabViewSample; + newWindow.AppWindow.SetIcon("Assets/Tiles/GalleryIcon.ico"); + tabViewSample.LoadDemoData(); + tabViewSample.SetupWindowMinSize(newWindow); + + newWindow.Activate(); + } + + private void TabViewContextMenu_Opening(object sender, object e) + { + TabViewHelper.PopulateTabViewContextMenu((MenuFlyout)sender); + } + } +} diff --git a/WinUIGallery/Helper/TabViewHelper.cs b/WinUIGallery/Helper/TabViewHelper.cs new file mode 100644 index 000000000..504b3295b --- /dev/null +++ b/WinUIGallery/Helper/TabViewHelper.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; + +namespace WinUIGallery.Helper +{ + public static class TabViewHelper + { + public static void PopulateTabViewContextMenu(MenuFlyout contextMenu) + { + contextMenu.Items.Clear(); + + var item = (TabViewItem)contextMenu.Target; + ListView tabViewListView = null; + TabView tabView = null; + + DependencyObject current = item; + + while (current != null) + { + DependencyObject parent = VisualTreeHelper.GetParent(current); + + if (parent is ListView parentTabViewListView) + { + tabViewListView = parentTabViewListView; + } + else if (parent is TabView parentTabView) + { + tabView = parentTabView; + } + + if (tabViewListView != null && tabView != null) + { + break; + } + + current = parent; + } + + if (tabViewListView == null || tabView == null) + { + return; + } + + // First, if there are tabs to the left or to the right of the tab on which this context menu is opening, + // then we'll include menu items to move this tab to the left or to the right. + // + // There are two possible cases for tab views: either they have explicitly set tab items, or they have a data item source set. + // To move a tab left or right with explicitly set tab items, we'll remove and replace the tab item itself. + // To move a tab left or right with a data item source set, we'll instead remove and replace the data item in the source list. + int index = tabViewListView.IndexFromContainer(item); + + if (index > 0) + { + MenuFlyoutItem moveLeftItem = new() { Text = "Move tab left" }; + moveLeftItem.Click += (s, args) => + { + if (tabView.TabItemsSource is IList itemsSourceList) + { + var item = itemsSourceList[index]; + itemsSourceList.RemoveAt(index); + itemsSourceList.Insert(index - 1, item); + } + else + { + var item = tabView.TabItems[index]; + tabView.TabItems.RemoveAt(index); + tabView.TabItems.Insert(index - 1, item); + } + }; + contextMenu.Items.Add(moveLeftItem); + } + + if (index < tabViewListView.Items.Count - 1) + { + MenuFlyoutItem moveRightItem = new() { Text = "Move tab right" }; + moveRightItem.Click += (s, args) => + { + if (tabView.TabItemsSource is IList itemsSourceList) + { + var item = itemsSourceList[index]; + itemsSourceList.RemoveAt(index); + itemsSourceList.Insert(index + 1, item); + } + else + { + var item = tabView.TabItems[index]; + tabView.TabItems.RemoveAt(index); + tabView.TabItems.Insert(index + 1, item); + } + }; + contextMenu.Items.Add(moveRightItem); + } + } + } +} diff --git a/WinUIGallery/Helper/UIHelper.cs b/WinUIGallery/Helper/UIHelper.cs index 2d17a985a..49c0b327a 100644 --- a/WinUIGallery/Helper/UIHelper.cs +++ b/WinUIGallery/Helper/UIHelper.cs @@ -63,5 +63,22 @@ static public void AnnounceActionForAccessibility(UIElement ue, string annouceme peer.RaiseNotificationEvent(AutomationNotificationKind.ActionCompleted, AutomationNotificationProcessing.ImportantMostRecent, annoucement, activityID); } + + public static T GetParent(DependencyObject child) where T : DependencyObject + { + DependencyObject current = child; + + while (current != null) + { + if (current is T parent) + { + return parent; + } + + current = VisualTreeHelper.GetParent(current); + } + + return null; + } } } diff --git a/WinUIGallery/Helper/Win32WindowHelper.cs b/WinUIGallery/Helper/Win32WindowHelper.cs index 79e5d2778..ca10f919a 100644 --- a/WinUIGallery/Helper/Win32WindowHelper.cs +++ b/WinUIGallery/Helper/Win32WindowHelper.cs @@ -7,8 +7,8 @@ namespace WinUIGallery.Helper { internal class Win32WindowHelper { - private static WinProc newWndProc = null; - private static nint oldWndProc = nint.Zero; + private WinProc newWndProc = null; + private nint oldWndProc = nint.Zero; private POINT? minWindowSize = null; private POINT? maxWindowSize = null; diff --git a/WinUIGallery/TabViewPages/MyTabContentControl.xaml b/WinUIGallery/TabViewPages/MyTabContentControl.xaml index 70ec6f135..cb9ab5afa 100644 --- a/WinUIGallery/TabViewPages/MyTabContentControl.xaml +++ b/WinUIGallery/TabViewPages/MyTabContentControl.xaml @@ -13,7 +13,7 @@ - - + + diff --git a/WinUIGallery/TabViewPages/MyTabContentControl.xaml.cs b/WinUIGallery/TabViewPages/MyTabContentControl.xaml.cs index 4d3072913..21ce40ce0 100644 --- a/WinUIGallery/TabViewPages/MyTabContentControl.xaml.cs +++ b/WinUIGallery/TabViewPages/MyTabContentControl.xaml.cs @@ -7,6 +7,14 @@ namespace WinUIGallery.TabViewPages { public sealed partial class MyTabContentControl : UserControl { + public bool IsInProgress + { + get { return (bool)GetValue(IsInProgressProperty); } + set { SetValue(IsInProgressProperty, value); } + } + + public static readonly DependencyProperty IsInProgressProperty = DependencyProperty.Register("IsInProgress", typeof(bool), typeof(MyTabContentControl), new PropertyMetadata(false)); + public MyTabContentControl() { this.InitializeComponent(); diff --git a/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml b/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml index 38c0658da..55baecb70 100644 --- a/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml +++ b/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml @@ -17,13 +17,27 @@ TabTearOutWindowRequested="Tabs_TabTearOutWindowRequested" TabTearOutRequested="Tabs_TabTearOutRequested" ExternalTornOutTabsDropping="Tabs_ExternalTornOutTabsDropping" - ExternalTornOutTabsDropped="Tabs_ExternalTornOutTabsDropped"> + ExternalTornOutTabsDropped="Tabs_ExternalTornOutTabsDropped" + TabItemsSource="{x:Bind TabItemDataList}"> + + + + + + + + + + + + + diff --git a/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml.cs b/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml.cs index 00d6e878c..15e86ec0f 100644 --- a/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml.cs +++ b/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml.cs @@ -1,30 +1,49 @@ -using System; -using Windows.ApplicationModel.Core; -using Windows.ApplicationModel.DataTransfer; -using Windows.Foundation.Metadata; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Input; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Hosting; -using Microsoft.UI.Xaml.Navigation; -using Microsoft.UI.Windowing; -using WinUIGallery.Helper; -using System.Threading; -using Microsoft.UI.Dispatching; -using System.Threading.Tasks; -using Windows.System; -using DispatcherQueueHandler = Microsoft.UI.Dispatching.DispatcherQueueHandler; -using System.Linq; using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Input; +using Windows.System; +using WinUIGallery.Helper; namespace WinUIGallery.TabViewPages { + public class TabItemData : DependencyObject + { + public string Header + { + get { return (string)GetValue(HeaderProperty); } + set { SetValue(HeaderProperty, value); } + } + + public string Content + { + get { return (string)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + public bool IsInProgress + { + get { return (bool)GetValue(IsInProgressProperty); } + set { SetValue(IsInProgressProperty, value); } + } + + public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register("Header", typeof(string), typeof(TabItemData), new PropertyMetadata("")); + public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(string), typeof(TabItemData), new PropertyMetadata("")); + public static readonly DependencyProperty IsInProgressProperty = DependencyProperty.Register("Content", typeof(bool), typeof(TabItemData), new PropertyMetadata(false)); + } + public sealed partial class TabViewWindowingSamplePage : Page { - private const string DataIdentifier = "MyTabItem"; + private static readonly List windowList = []; + private static Window tabTearOutWindow = null; + private Win32WindowHelper win32WindowHelper; - private Window tabTearOutWindow = null; + + private readonly ObservableCollection tabItemDataList = []; + public ObservableCollection TabItemDataList => tabItemDataList; public TabViewWindowingSamplePage() { @@ -45,6 +64,35 @@ private void TabViewWindowingSamplePage_Loaded(object sender, RoutedEventArgs e) currentWindow.ExtendsContentIntoTitleBar = true; currentWindow.SetTitleBar(CustomDragRegion); CustomDragRegion.MinWidth = 188; + + if (!windowList.Contains(currentWindow)) + { + windowList.Add(currentWindow); + + // We can have a window we're dragging in two different ways: either we created a new window + // for tearing out purposes, or we're dragging an existing window. + // If we created a new window, tabTearOutWindow will be set to that window. + // Otherwise, it won't be set to anything, so we should set it to the window we're currently dragging. + var inputNonClientPointerSource = InputNonClientPointerSource.GetForWindowId(currentWindow.AppWindow.Id); + + inputNonClientPointerSource.EnteredMoveSize += (s, args) => + { + if (tabTearOutWindow == null) + { + tabTearOutWindow = currentWindow; + } + }; + + inputNonClientPointerSource.ExitedMoveSize += (s, args) => + { + tabTearOutWindow = null; + }; + + currentWindow.Closed += (s, args) => + { + windowList.Remove(currentWindow); + }; + } } public void LoadDemoData() @@ -52,27 +100,15 @@ public void LoadDemoData() // Main Window -- add some default items for (int i = 0; i < 3; i++) { - Tabs.TabItems.Add(new TabViewItem() { IconSource = new Microsoft.UI.Xaml.Controls.SymbolIconSource() { Symbol = Symbol.Placeholder }, Header = $"Item {i}", Content = new MyTabContentControl() { DataContext = $"Page {i}" } }); + TabItemDataList.Add(new TabItemData() { Header = $"Item {i}", Content = $"Page {i}" }); } Tabs.SelectedIndex = 0; } - public void AddTabToTabs(TabViewItem tab) - { - Tabs.TabItems.Add(tab); - } - private void Tabs_TabTearOutWindowRequested(TabView sender, TabViewTabTearOutWindowRequestedEventArgs args) { - var newPage = new TabViewWindowingSamplePage(); - - tabTearOutWindow = WindowHelper.CreateWindow(); - tabTearOutWindow.ExtendsContentIntoTitleBar = true; - tabTearOutWindow.Content = newPage; - tabTearOutWindow.AppWindow.SetIcon("Assets/Tiles/GalleryIcon.ico"); - newPage.SetupWindowMinSize(tabTearOutWindow); - + tabTearOutWindow = CreateNewWindow(); args.NewWindowId = tabTearOutWindow.AppWindow.Id; } @@ -83,13 +119,7 @@ private void Tabs_TabTearOutRequested(TabView sender, TabViewTabTearOutRequested return; } - var newPage = (TabViewWindowingSamplePage)tabTearOutWindow.Content; - - foreach (TabViewItem tab in args.Tabs.Cast()) - { - GetParentTabView(tab)?.TabItems.Remove(tab); - newPage.AddTabToTabs(tab); - } + MoveDataItems(TabItemDataList, GetTabItemDataList(tabTearOutWindow), args.Items, 0); } private void Tabs_ExternalTornOutTabsDropping(TabView sender, TabViewExternalTornOutTabsDroppingEventArgs args) @@ -99,65 +129,175 @@ private void Tabs_ExternalTornOutTabsDropping(TabView sender, TabViewExternalTor private void Tabs_ExternalTornOutTabsDropped(TabView sender, TabViewExternalTornOutTabsDroppedEventArgs args) { - int position = 0; - - foreach (TabViewItem tab in args.Tabs.Cast()) - { - GetParentTabView(tab)?.TabItems.Remove(tab); - sender.TabItems.Insert(args.DropIndex + position, tab); - position++; - } + MoveDataItems(GetTabItemDataList(tabTearOutWindow), TabItemDataList, args.Items, args.DropIndex); } - private TabView GetParentTabView(TabViewItem tab) + private static Window CreateNewWindow() { - DependencyObject current = tab; + var newPage = new TabViewWindowingSamplePage(); + + var window = WindowHelper.CreateWindow(); + window.ExtendsContentIntoTitleBar = true; + window.Content = newPage; + window.AppWindow.SetIcon("Assets/Tiles/GalleryIcon.ico"); + newPage.SetupWindowMinSize(window); - while (current != null) + return window; + } + + private static void MoveDataItems(ObservableCollection source, ObservableCollection destination, object[] dataItems, int index) + { + foreach (object tabItemData in dataItems) { - if (current is TabView tabView) - { - return tabView; - } + source.Remove((TabItemData)tabItemData); + destination.Insert(index, (TabItemData)tabItemData); - current = VisualTreeHelper.GetParent(current); + index++; } - - return null; } - private TabViewItem CreateNewTVI(string header, string dataContext) + private static TabView GetTabView(Window window) { - var newTab = new TabViewItem() - { - IconSource = new Microsoft.UI.Xaml.Controls.SymbolIconSource() - { - Symbol = Symbol.Placeholder - }, - Header = header, - Content = new MyTabContentControl() - { - DataContext = dataContext - } - }; + var tabViewPage = (TabViewWindowingSamplePage)window.Content; + return tabViewPage.Tabs; + } - return newTab; + private static ObservableCollection GetTabItemDataList(Window window) + { + var tabViewPage = (TabViewWindowingSamplePage)window.Content; + return tabViewPage.TabItemDataList; } private void Tabs_AddTabButtonClick(TabView sender, object args) { - var tab = CreateNewTVI("New Item", "New Item"); - sender.TabItems.Add(tab); + TabItemDataList.Add(new TabItemData() { Header = "New Item", Content = "New Item" }); } private void Tabs_TabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) { - sender.TabItems.Remove(args.Tab); + TabItemDataList.Remove((TabItemData)args.Item); - if (sender.TabItems.Count == 0) + if (TabItemDataList.Count == 0) { WindowHelper.GetWindowForElement(this).Close(); } } + + private void TabViewContextMenu_Opening(object sender, object e) + { + // The contents of the context menu depends on the state of the application, so we'll build it dynamically. + MenuFlyout contextMenu = (MenuFlyout)sender; + + // We'll first put the generic tab view context menu items in place. + TabViewHelper.PopulateTabViewContextMenu(contextMenu); + + var tabViewItem = (TabViewItem)contextMenu.Target; + ListView tabViewListView = UIHelper.GetParent(tabViewItem); + var window = WindowHelper.GetWindowForElement(tabViewItem); + + if (tabViewListView == null) + { + return; + } + + var tabItemDataList = GetTabItemDataList(window); + var tabDataItem = tabViewListView.ItemFromContainer(tabViewItem); + + // Second, we'll include menu items to move this tab to those windows. + MenuFlyoutSubItem moveSubItem = new() { Text = "Move tab to" }; + + // If there are at least two tabs in this window, we'll include the option to move the tab to a new window. + // This option doesn't make sense if there is only one tab, because in that case the source window would have no tabs left, + // and we would effectively be just moving the tab from one window with only one tab to another window with only one tab, + // leaving us in the same state as we started in. + if (tabItemDataList.Count > 1) + { + MenuFlyoutItem newWindowItem = new() { Text = "New window", Icon = new SymbolIcon(Symbol.NewWindow) }; + + newWindowItem.Click += (s, args) => + { + var newWindow = CreateNewWindow(); + MoveDataItems(tabItemDataList, GetTabItemDataList(newWindow), [tabDataItem], 0); + + // Activating the window and setting its selected item hit a failed assert if the content hasn't been loaded yet, + // so we'll defer these for a tick to allow that to happen first. + DispatcherQueue.TryEnqueue(() => + { + newWindow.Activate(); + GetTabView(newWindow).SelectedItem = tabDataItem; + }); + }; + + moveSubItem.Items.Add(newWindowItem); + } + + // If there are other windows that exist, we'll include the option to move the tab to those windows. + List moveToWindowItems = []; + + foreach (Window otherWindow in windowList) + { + if (window == otherWindow) + { + continue; + } + + var windowTabItemDataList = GetTabItemDataList(otherWindow); + + if (windowTabItemDataList.Count > 0) + { + string moveToWindowItemText = $"Window with \"{windowTabItemDataList[0].Header}\""; + + if (windowTabItemDataList.Count > 1) + { + int remainingTabCount = windowTabItemDataList.Count - 1; + moveToWindowItemText += $" and {remainingTabCount} other tab{(remainingTabCount == 1 ? "" : "s")}"; + } + + MenuFlyoutItem moveToWindowItem = new() { Text = moveToWindowItemText, Icon = new SymbolIcon(Symbol.BackToWindow) }; + moveToWindowItem.Click += (s, args) => + { + MoveDataItems(tabItemDataList, windowTabItemDataList, [tabDataItem], windowTabItemDataList.Count); + + // If removing the tab from its current tab view will leave no tabs remaining, then we'll close the tab view's window. + if (tabItemDataList.Count == 0) + { + window.Close(); + } + + // Activating the window and setting its selected item hit a failed assert if the content hasn't been loaded yet, + // so we'll defer these for a tick to allow that to happen first. + DispatcherQueue.TryEnqueue(() => + { + otherWindow.Activate(); + GetTabView(otherWindow).SelectedItem = tabDataItem; + }); + }; + moveToWindowItems.Add(moveToWindowItem); + } + } + + // Only include a separator if we're going to be including at least one move-to-window item. + if (moveToWindowItems.Count > 0) + { + contextMenu.Items.Add(new MenuFlyoutSeparator()); + } + + foreach (MenuFlyoutItem moveToWindowItem in moveToWindowItems) + { + moveSubItem.Items.Add(moveToWindowItem); + } + + // Only include the move-to sub-item if it has any items. + if (moveSubItem.Items.Count > 0) + { + contextMenu.Items.Add(moveSubItem); + } + + // If the context menu ended up with no items at all, then we'll prevent it from being shown. + if (contextMenu.Items.Count == 0) + { + contextMenu.Hide(); + } + } } } diff --git a/WinUIGallery/WinUIGallery.csproj b/WinUIGallery/WinUIGallery.csproj index b4defb30b..6f22bb2eb 100644 --- a/WinUIGallery/WinUIGallery.csproj +++ b/WinUIGallery/WinUIGallery.csproj @@ -313,4 +313,8 @@ + + + + \ No newline at end of file