From 06234fbeec7e3b16cda8a03d89f2a9a00dae5389 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Tue, 25 Nov 2025 23:17:28 +0900 Subject: [PATCH 01/24] Add Tabs --- .../Pages/Navigation/TabControlPage.xaml | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml index 4fb349692..52b898520 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml @@ -46,6 +46,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 32768699e10ce4a8202873f8ece55d4d3c862222 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Tue, 25 Nov 2025 23:54:09 +0900 Subject: [PATCH 02/24] Add drag-and-drop support to Standard TabControl in Gallery --- .../Pages/Navigation/TabControlViewModel.cs | 94 ++++++++++- .../Pages/Navigation/TabControlPage.xaml | 146 ++---------------- .../Pages/Navigation/TabControlPage.xaml.cs | 120 +++++++++++++- 3 files changed, 225 insertions(+), 135 deletions(-) diff --git a/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs b/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs index cdb2979d8..b00448460 100644 --- a/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs +++ b/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs @@ -3,6 +3,98 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +using System.Collections.ObjectModel; +using System.Windows.Controls; +using Wpf.Ui.Controls; + namespace Wpf.Ui.Gallery.ViewModels.Pages.Navigation; -public partial class TabControlViewModel : ViewModel; +public partial class TabControlViewModel : ViewModel +{ + [ObservableProperty] + private ObservableCollection _standardTabs = new() + { + new TabItem + { + Header = CreateTabHeader("Hello", SymbolRegular.XboxConsole24), + Content = new System.Windows.Controls.TextBlock { Text = "World", Margin = new System.Windows.Thickness(12) }, + IsSelected = true + }, + new TabItem + { + Header = CreateTabHeader("The cake", SymbolRegular.StoreMicrosoft16), + Content = new System.Windows.Controls.TextBlock { Text = "Is a lie.", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("Document", SymbolRegular.Document24), + Content = new System.Windows.Controls.TextBlock { Text = "Document content", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("Settings", SymbolRegular.Settings24), + Content = new System.Windows.Controls.TextBlock { Text = "Settings content", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("Mail", SymbolRegular.Mail24), + Content = new System.Windows.Controls.TextBlock { Text = "Mail content", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("Calendar", SymbolRegular.CalendarLtr24), + Content = new System.Windows.Controls.TextBlock { Text = "Calendar content", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("Image", SymbolRegular.Image24), + Content = new System.Windows.Controls.TextBlock { Text = "Image content", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("Music", SymbolRegular.MusicNote124), + Content = new System.Windows.Controls.TextBlock { Text = "Music content", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("Video", SymbolRegular.Video24), + Content = new System.Windows.Controls.TextBlock { Text = "Video content", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("Folder", SymbolRegular.Folder24), + Content = new System.Windows.Controls.TextBlock { Text = "Folder content", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("tab 1", SymbolRegular.Folder24), + Content = new System.Windows.Controls.TextBlock { Text = "tab 1 content", Margin = new System.Windows.Thickness(12) } + }, + new TabItem + { + Header = CreateTabHeader("tab 2", SymbolRegular.Folder24), + Content = new System.Windows.Controls.TextBlock { Text = "tab 2 content", Margin = new System.Windows.Thickness(12) } + }, + }; + + private static System.Windows.Controls.StackPanel CreateTabHeader(string text, SymbolRegular symbol) + { + return new System.Windows.Controls.StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + Children = + { + new SymbolIcon + { + Symbol = symbol, + Margin = new System.Windows.Thickness(0, 0, 6, 0) + }, + new System.Windows.Controls.TextBlock + { + Text = text + } + } + }; + } +} diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml index 52b898520..0cfb82fec 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml @@ -23,139 +23,19 @@ Margin="0" HeaderText="Standard TabControl." XamlCode="<TabControl />"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs index 0daa5c122..eaf252c04 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs @@ -1,8 +1,12 @@ -// This Source Code Form is subject to the terms of the MIT License. +// This Source Code Form is subject to the terms of the MIT License. // If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; using Wpf.Ui.Controls; using Wpf.Ui.Gallery.ControlsLookup; using Wpf.Ui.Gallery.ViewModels.Pages.Navigation; @@ -14,6 +18,12 @@ public partial class TabControlPage : INavigableView { public TabControlViewModel ViewModel { get; } + // Stores the tab being dragged during drag-and-drop operation + private TabItem? _standardDraggedTab; + + // Stores the starting point of the drag operation + private Point _standardStartPoint; + public TabControlPage(TabControlViewModel viewModel) { ViewModel = viewModel; @@ -21,4 +31,112 @@ public TabControlPage(TabControlViewModel viewModel) InitializeComponent(); } + + /// + /// Handles the mouse left button down event on a tab item. + /// Selects the clicked tab and prepares for potential drag operation. + /// + private void StandardTabItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (sender is TabItem tabItem && tabItem != null) + { + // Deselect all other tabs to ensure only one tab is selected at a time + foreach (TabItem tab in ViewModel.StandardTabs) + { + if (tab != tabItem && tab.IsSelected) + { + tab.SetCurrentValue(TabItem.IsSelectedProperty, false); + } + } + + // Select the clicked tab before starting drag operation + tabItem.SetCurrentValue(TabItem.IsSelectedProperty, true); + _standardDraggedTab = tabItem; + _standardStartPoint = e.GetPosition(null); + } + } + + /// + /// Handles the mouse move event during drag operation. + /// Initiates drag-and-drop when the mouse has moved beyond the minimum drag distance. + /// + private void StandardTabItem_PreviewMouseMove(object sender, MouseEventArgs e) + { + if (e.LeftButton == MouseButtonState.Pressed && _standardDraggedTab != null) + { + Point currentPoint = e.GetPosition(null); + + // Check if the mouse has moved far enough to initiate a drag operation + if (Math.Abs(currentPoint.X - _standardStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance || + Math.Abs(currentPoint.Y - _standardStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance) + { + DragDrop.DoDragDrop(_standardDraggedTab, _standardDraggedTab, DragDropEffects.Move); + } + } + } + + /// + /// Handles the mouse left button up event. + /// Clears the dragged tab reference when the drag operation ends. + /// + private void StandardTabItem_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + _standardDraggedTab = null; + } + + /// + /// Handles the drop event when a tab is dropped onto the TabControl. + /// Reorders the tabs in the collection based on the drop position. + /// + private void StandardTabControl_Drop(object sender, DragEventArgs e) + { + if (_standardDraggedTab == null || sender is not TabControl tabControl) + { + return; + } + + // Find the tab item at the drop position + HitTestResult hitTestResult = VisualTreeHelper.HitTest(tabControl, e.GetPosition(tabControl)); + if (hitTestResult?.VisualHit == null) + { + return; + } + + // Get the target tab item from the visual tree + TabItem? targetTabItem = FindParent(hitTestResult.VisualHit); + if (targetTabItem == null || targetTabItem == _standardDraggedTab) + { + return; + } + + // Reorder tabs: remove from original position and insert at target position + ObservableCollection tabs = ViewModel.StandardTabs; + int draggedIndex = tabs.IndexOf(_standardDraggedTab); + int targetIndex = tabs.IndexOf(targetTabItem); + + if (draggedIndex >= 0 && targetIndex >= 0 && draggedIndex != targetIndex) + { + tabs.RemoveAt(draggedIndex); + tabs.Insert(targetIndex, _standardDraggedTab); + + // Maintain selection state after drop to keep the moved tab selected + _standardDraggedTab.SetCurrentValue(TabItem.IsSelectedProperty, true); + } + } + + private static T? FindParent(DependencyObject child) where T : DependencyObject + { + DependencyObject parentObject = VisualTreeHelper.GetParent(child); + if (parentObject == null) + { + return null; + } + + if (parentObject is T parent) + { + return parent; + } + + return FindParent(parentObject); + } } From 3296f0efd2935ff18ef682cd2df120159343aa1f Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Wed, 26 Nov 2025 01:49:18 +0900 Subject: [PATCH 03/24] Prevented errors when PreviewMouseMove is called during dragging --- .../Views/Pages/Navigation/TabControlPage.xaml.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs index eaf252c04..4ea51ce3d 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs @@ -24,12 +24,21 @@ public partial class TabControlPage : INavigableView // Stores the starting point of the drag operation private Point _standardStartPoint; + // Indicates whether a drag operation is currently in progress + private bool _isDragging; + public TabControlPage(TabControlViewModel viewModel) { ViewModel = viewModel; DataContext = this; InitializeComponent(); + + // Ensure the first tab is selected and its content is displayed + if (ViewModel.StandardTabs.Count > 0 && StandardTabControl != null) + { + StandardTabControl.SelectedItem = ViewModel.StandardTabs[0]; + } } /// @@ -62,7 +71,7 @@ private void StandardTabItem_PreviewMouseLeftButtonDown(object sender, MouseButt /// private void StandardTabItem_PreviewMouseMove(object sender, MouseEventArgs e) { - if (e.LeftButton == MouseButtonState.Pressed && _standardDraggedTab != null) + if (e.LeftButton == MouseButtonState.Pressed && _standardDraggedTab != null && !_isDragging) { Point currentPoint = e.GetPosition(null); @@ -70,7 +79,9 @@ private void StandardTabItem_PreviewMouseMove(object sender, MouseEventArgs e) if (Math.Abs(currentPoint.X - _standardStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(currentPoint.Y - _standardStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance) { + _isDragging = true; DragDrop.DoDragDrop(_standardDraggedTab, _standardDraggedTab, DragDropEffects.Move); + _isDragging = false; } } } @@ -82,6 +93,7 @@ private void StandardTabItem_PreviewMouseMove(object sender, MouseEventArgs e) private void StandardTabItem_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _standardDraggedTab = null; + _isDragging = false; } /// From 6cf30bb3a927ceba15a961822737dfbce62ca959 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Thu, 27 Nov 2025 05:34:59 +0900 Subject: [PATCH 04/24] Reorder tabs: move from original position to target position --- .../Views/Pages/Navigation/TabControlPage.xaml.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs index 4ea51ce3d..b00ba1bc7 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs @@ -3,6 +3,7 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +using System.Collections.ObjectModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -121,15 +122,14 @@ private void StandardTabControl_Drop(object sender, DragEventArgs e) return; } - // Reorder tabs: remove from original position and insert at target position + // Reorder tabs: move from original position to target position ObservableCollection tabs = ViewModel.StandardTabs; int draggedIndex = tabs.IndexOf(_standardDraggedTab); int targetIndex = tabs.IndexOf(targetTabItem); if (draggedIndex >= 0 && targetIndex >= 0 && draggedIndex != targetIndex) { - tabs.RemoveAt(draggedIndex); - tabs.Insert(targetIndex, _standardDraggedTab); + tabs.Move(draggedIndex, targetIndex); // Maintain selection state after drop to keep the moved tab selected _standardDraggedTab.SetCurrentValue(TabItem.IsSelectedProperty, true); From 39abcb67e25eb2a3a27fe80fd8415fd5d6c4fc25 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Thu, 27 Nov 2025 06:30:55 +0900 Subject: [PATCH 05/24] MVVM compliant --- .../Pages/Navigation/TabControlViewModel.cs | 152 ++++++++++++++++++ .../Pages/Navigation/TabControlPage.xaml | 101 +++++++++++- .../Pages/Navigation/TabControlPage.xaml.cs | 118 ++++++-------- 3 files changed, 300 insertions(+), 71 deletions(-) diff --git a/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs b/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs index b00448460..c6d3630b3 100644 --- a/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs +++ b/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs @@ -3,7 +3,9 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +using System; using System.Collections.ObjectModel; +using System.Windows; using System.Windows.Controls; using Wpf.Ui.Controls; @@ -11,6 +13,26 @@ namespace Wpf.Ui.Gallery.ViewModels.Pages.Navigation; public partial class TabControlViewModel : ViewModel { + [ObservableProperty] + private TabItem? _selectedTab; + + // Stores the tab being dragged during drag-and-drop operation + private TabItem? _draggedTab; + + // Stores the starting point of the drag operation + private Point _dragStartPoint; + + // Indicates whether a drag operation is currently in progress + private bool _isDragging; + + partial void OnSelectedTabChanged(TabItem? value) + { + foreach (var tab in StandardTabs) + { + tab.SetCurrentValue(TabItem.IsSelectedProperty, tab == value); + } + } + [ObservableProperty] private ObservableCollection _standardTabs = new() { @@ -77,6 +99,136 @@ public partial class TabControlViewModel : ViewModel }, }; + /// + /// Adds a new tab to the collection. + /// + [RelayCommand] + private void AddTab() + { + var tabNumber = StandardTabs.Count + 1; + var newTab = new TabItem + { + Header = CreateTabHeader($"New Tab {tabNumber}", SymbolRegular.Document24), + Content = new System.Windows.Controls.TextBlock + { + Text = $"New Tab {tabNumber} content", + Margin = new System.Windows.Thickness(12) + } + }; + + StandardTabs.Add(newTab); + SelectedTab = newTab; + } + + /// + /// Closes the specified tab. + /// + /// The tab item to close. + [RelayCommand] + private void CloseTab(object? tabItem) + { + if (tabItem is not TabItem item || StandardTabs.Count <= 1) + { + return; + } + + var tabIndex = StandardTabs.IndexOf(item); + if (tabIndex < 0) + { + return; + } + + StandardTabs.RemoveAt(tabIndex); + + if (StandardTabs.Count > 0) + { + SelectedTab = StandardTabs[Math.Min(tabIndex, StandardTabs.Count - 1)]; + } + } + + /// + /// Selects the specified tab and prepares for potential drag operation. + /// + /// The tab item to select. + [RelayCommand] + private void SelectTabForDrag(object? parameter) + { + if (parameter is TabItem tabItem) + { + SelectedTab = tabItem; + _draggedTab = tabItem; + } + } + + /// + /// Starts the drag operation if the mouse has moved far enough. + /// + /// The current mouse position. + /// True if drag operation was started, false otherwise. + public bool TryStartDrag(Point currentPoint) + { + if (_draggedTab == null || _isDragging) + { + return false; + } + + var deltaX = Math.Abs(currentPoint.X - _dragStartPoint.X); + var deltaY = Math.Abs(currentPoint.Y - _dragStartPoint.Y); + var minDistance = SystemParameters.MinimumHorizontalDragDistance; + + if (deltaX > minDistance || deltaY > minDistance) + { + _isDragging = true; + return true; + } + + return false; + } + + /// + /// Gets the tab being dragged. + /// + public TabItem? GetDraggedTab() => _draggedTab; + + /// + /// Sets the starting point for drag operation. + /// + public void SetDragStartPoint(Point point) + { + _dragStartPoint = point; + } + + /// + /// Ends the drag operation. + /// + public void EndDrag() + { + _draggedTab = null; + _isDragging = false; + } + + /// + /// Reorders tabs by moving a tab from one position to another. + /// + /// The tab being moved. + /// The target tab position. + public void ReorderTabs(TabItem draggedTab, TabItem targetTab) + { + if (draggedTab == targetTab) + { + return; + } + + var draggedIndex = StandardTabs.IndexOf(draggedTab); + var targetIndex = StandardTabs.IndexOf(targetTab); + + if (draggedIndex >= 0 && targetIndex >= 0 && draggedIndex != targetIndex) + { + StandardTabs.Move(draggedIndex, targetIndex); + SelectedTab = draggedTab; + } + } + private static System.Windows.Controls.StackPanel CreateTabHeader(string text, SymbolRegular symbol) { return new System.Windows.Controls.StackPanel diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml index 0cfb82fec..640237b8e 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml @@ -23,14 +23,103 @@ Margin="0" HeaderText="Standard TabControl." XamlCode="<TabControl />"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs index b00ba1bc7..e2a4828a1 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs @@ -3,7 +3,6 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. -using System.Collections.ObjectModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -19,26 +18,16 @@ public partial class TabControlPage : INavigableView { public TabControlViewModel ViewModel { get; } - // Stores the tab being dragged during drag-and-drop operation - private TabItem? _standardDraggedTab; - - // Stores the starting point of the drag operation - private Point _standardStartPoint; - - // Indicates whether a drag operation is currently in progress - private bool _isDragging; - public TabControlPage(TabControlViewModel viewModel) { ViewModel = viewModel; - DataContext = this; - + DataContext = viewModel; InitializeComponent(); - // Ensure the first tab is selected and its content is displayed - if (ViewModel.StandardTabs.Count > 0 && StandardTabControl != null) + // Ensure the first tab is selected + if (ViewModel.StandardTabs.Count > 0) { - StandardTabControl.SelectedItem = ViewModel.StandardTabs[0]; + ViewModel.SelectedTab = ViewModel.StandardTabs[0]; } } @@ -48,22 +37,13 @@ public TabControlPage(TabControlViewModel viewModel) /// private void StandardTabItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { - if (sender is TabItem tabItem && tabItem != null) + if (IsCloseButton(e.OriginalSource) || sender is not TabItem tabItem) { - // Deselect all other tabs to ensure only one tab is selected at a time - foreach (TabItem tab in ViewModel.StandardTabs) - { - if (tab != tabItem && tab.IsSelected) - { - tab.SetCurrentValue(TabItem.IsSelectedProperty, false); - } - } - - // Select the clicked tab before starting drag operation - tabItem.SetCurrentValue(TabItem.IsSelectedProperty, true); - _standardDraggedTab = tabItem; - _standardStartPoint = e.GetPosition(null); + return; } + + ViewModel.SetDragStartPoint(e.GetPosition(null)); + ViewModel.SelectTabForDragCommand.Execute(tabItem); } /// @@ -72,17 +52,18 @@ private void StandardTabItem_PreviewMouseLeftButtonDown(object sender, MouseButt /// private void StandardTabItem_PreviewMouseMove(object sender, MouseEventArgs e) { - if (e.LeftButton == MouseButtonState.Pressed && _standardDraggedTab != null && !_isDragging) + if (IsCloseButton(e.OriginalSource) || e.LeftButton != MouseButtonState.Pressed) { - Point currentPoint = e.GetPosition(null); + return; + } - // Check if the mouse has moved far enough to initiate a drag operation - if (Math.Abs(currentPoint.X - _standardStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance || - Math.Abs(currentPoint.Y - _standardStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance) + if (ViewModel.TryStartDrag(e.GetPosition(null))) + { + TabItem? draggedTab = ViewModel.GetDraggedTab(); + if (draggedTab != null) { - _isDragging = true; - DragDrop.DoDragDrop(_standardDraggedTab, _standardDraggedTab, DragDropEffects.Move); - _isDragging = false; + DragDrop.DoDragDrop(draggedTab, draggedTab, DragDropEffects.Move); + ViewModel.EndDrag(); } } } @@ -93,8 +74,8 @@ private void StandardTabItem_PreviewMouseMove(object sender, MouseEventArgs e) /// private void StandardTabItem_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { - _standardDraggedTab = null; - _isDragging = false; + // Use ViewModel to end drag operation + ViewModel.EndDrag(); } /// @@ -103,52 +84,59 @@ private void StandardTabItem_PreviewMouseLeftButtonUp(object sender, MouseButton /// private void StandardTabControl_Drop(object sender, DragEventArgs e) { - if (_standardDraggedTab == null || sender is not TabControl tabControl) + TabItem? draggedTab = ViewModel.GetDraggedTab(); + if (draggedTab == null || sender is not TabControl tabControl) { return; } - // Find the tab item at the drop position HitTestResult hitTestResult = VisualTreeHelper.HitTest(tabControl, e.GetPosition(tabControl)); - if (hitTestResult?.VisualHit == null) - { - return; - } + TabItem? targetTabItem = hitTestResult?.VisualHit != null ? FindParent(hitTestResult.VisualHit) : null; - // Get the target tab item from the visual tree - TabItem? targetTabItem = FindParent(hitTestResult.VisualHit); - if (targetTabItem == null || targetTabItem == _standardDraggedTab) + if (targetTabItem != null) { - return; + ViewModel.ReorderTabs(draggedTab, targetTabItem); } + } - // Reorder tabs: move from original position to target position - ObservableCollection tabs = ViewModel.StandardTabs; - int draggedIndex = tabs.IndexOf(_standardDraggedTab); - int targetIndex = tabs.IndexOf(targetTabItem); - - if (draggedIndex >= 0 && targetIndex >= 0 && draggedIndex != targetIndex) + private static T? FindParent(DependencyObject child) + where T : DependencyObject + { + DependencyObject current = VisualTreeHelper.GetParent(child); + while (current != null) { - tabs.Move(draggedIndex, targetIndex); + if (current is T parent) + { + return parent; + } - // Maintain selection state after drop to keep the moved tab selected - _standardDraggedTab.SetCurrentValue(TabItem.IsSelectedProperty, true); + current = VisualTreeHelper.GetParent(current); } + + return null; } - private static T? FindParent(DependencyObject child) where T : DependencyObject + /// + /// Determines if the specified element is a close button. + /// + private static bool IsCloseButton(object? source) { - DependencyObject parentObject = VisualTreeHelper.GetParent(child); - if (parentObject == null) + if (source is not DependencyObject depObj) { - return null; + return false; } - if (parentObject is T parent) + DependencyObject current = depObj; + while (current != null) { - return parent; + if (current is System.Windows.Controls.Button { Tag: "CloseButton" }) + { + return true; + } + + current = VisualTreeHelper.GetParent(current); } - return FindParent(parentObject); + return false; } } From 4a249fb35880b995bf339a2f76c2cd3832b1e813 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Thu, 27 Nov 2025 21:41:58 +0900 Subject: [PATCH 06/24] Optimization --- .../Pages/Navigation/TabControlViewModel.cs | 64 +++++++++++++------ .../Pages/Navigation/TabControlPage.xaml | 1 + .../Pages/Navigation/TabControlPage.xaml.cs | 49 +++++++++----- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs b/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs index c6d3630b3..f0f4f6328 100644 --- a/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs +++ b/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs @@ -27,9 +27,14 @@ public partial class TabControlViewModel : ViewModel partial void OnSelectedTabChanged(TabItem? value) { - foreach (var tab in StandardTabs) + // Update IsSelected property only for tabs that need to change + foreach (TabItem tab in StandardTabs) { - tab.SetCurrentValue(TabItem.IsSelectedProperty, tab == value); + bool shouldBeSelected = tab == value; + if (tab.IsSelected != shouldBeSelected) + { + tab.SetCurrentValue(TabItem.IsSelectedProperty, shouldBeSelected); + } } } @@ -105,7 +110,8 @@ partial void OnSelectedTabChanged(TabItem? value) [RelayCommand] private void AddTab() { - var tabNumber = StandardTabs.Count + 1; + // Create a new tab with a unique name + int tabNumber = StandardTabs.Count + 1; var newTab = new TabItem { Header = CreateTabHeader($"New Tab {tabNumber}", SymbolRegular.Document24), @@ -116,7 +122,10 @@ private void AddTab() } }; + // Add the new tab to the collection StandardTabs.Add(newTab); + + // Select the new tab (this will update IsSelected for all tabs via OnSelectedTabChanged) SelectedTab = newTab; } @@ -127,37 +136,50 @@ private void AddTab() [RelayCommand] private void CloseTab(object? tabItem) { - if (tabItem is not TabItem item || StandardTabs.Count <= 1) + if (tabItem is not TabItem item) { return; } - var tabIndex = StandardTabs.IndexOf(item); + if (StandardTabs.Count <= 1) + { + // Don't remove the last tab + return; + } + + int tabIndex = StandardTabs.IndexOf(item); if (tabIndex < 0) { return; } + // Remove the tab StandardTabs.RemoveAt(tabIndex); + // Select another tab (preferably the one at the same index, or the last one) if (StandardTabs.Count > 0) { - SelectedTab = StandardTabs[Math.Min(tabIndex, StandardTabs.Count - 1)]; + int newSelectedIndex = Math.Min(tabIndex, StandardTabs.Count - 1); + SelectedTab = StandardTabs[newSelectedIndex]; } } /// /// Selects the specified tab and prepares for potential drag operation. /// - /// The tab item to select. + /// The tab item to select. + /// The starting point of the mouse operation. [RelayCommand] private void SelectTabForDrag(object? parameter) { - if (parameter is TabItem tabItem) + if (parameter is not TabItem tabItem) { - SelectedTab = tabItem; - _draggedTab = tabItem; + return; } + + // Select the clicked tab + SelectedTab = tabItem; + _draggedTab = tabItem; } /// @@ -172,11 +194,12 @@ public bool TryStartDrag(Point currentPoint) return false; } - var deltaX = Math.Abs(currentPoint.X - _dragStartPoint.X); - var deltaY = Math.Abs(currentPoint.Y - _dragStartPoint.Y); - var minDistance = SystemParameters.MinimumHorizontalDragDistance; + // Check if the mouse has moved far enough to initiate a drag operation + double deltaX = currentPoint.X - _dragStartPoint.X; + double deltaY = currentPoint.Y - _dragStartPoint.Y; + double minDistance = SystemParameters.MinimumHorizontalDragDistance; - if (deltaX > minDistance || deltaY > minDistance) + if (Math.Abs(deltaX) > minDistance || Math.Abs(deltaY) > minDistance) { _isDragging = true; return true; @@ -219,14 +242,17 @@ public void ReorderTabs(TabItem draggedTab, TabItem targetTab) return; } - var draggedIndex = StandardTabs.IndexOf(draggedTab); - var targetIndex = StandardTabs.IndexOf(targetTab); + int draggedIndex = StandardTabs.IndexOf(draggedTab); + int targetIndex = StandardTabs.IndexOf(targetTab); - if (draggedIndex >= 0 && targetIndex >= 0 && draggedIndex != targetIndex) + // Early return if indices are invalid or the same + if (draggedIndex < 0 || targetIndex < 0 || draggedIndex == targetIndex) { - StandardTabs.Move(draggedIndex, targetIndex); - SelectedTab = draggedTab; + return; } + + StandardTabs.Move(draggedIndex, targetIndex); + SelectedTab = draggedTab; } private static System.Windows.Controls.StackPanel CreateTabHeader(string text, SymbolRegular symbol) diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml index 640237b8e..8cd943434 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml @@ -24,6 +24,7 @@ HeaderText="Standard TabControl." XamlCode="<TabControl />"> 0) { ViewModel.SelectedTab = ViewModel.StandardTabs[0]; @@ -37,11 +38,13 @@ public TabControlPage(TabControlViewModel viewModel) /// private void StandardTabItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { + // Don't process if the close button was clicked if (IsCloseButton(e.OriginalSource) || sender is not TabItem tabItem) { return; } + // Use ViewModel to select the tab and prepare for drag ViewModel.SetDragStartPoint(e.GetPosition(null)); ViewModel.SelectTabForDragCommand.Execute(tabItem); } @@ -52,14 +55,18 @@ private void StandardTabItem_PreviewMouseLeftButtonDown(object sender, MouseButt /// private void StandardTabItem_PreviewMouseMove(object sender, MouseEventArgs e) { + // Don't process if the close button is being hovered or mouse button is not pressed if (IsCloseButton(e.OriginalSource) || e.LeftButton != MouseButtonState.Pressed) { return; } - if (ViewModel.TryStartDrag(e.GetPosition(null))) + Point currentPoint = e.GetPosition(null); + + // Use ViewModel to check if drag should start + if (ViewModel.TryStartDrag(currentPoint)) { - TabItem? draggedTab = ViewModel.GetDraggedTab(); + var draggedTab = ViewModel.GetDraggedTab(); if (draggedTab != null) { DragDrop.DoDragDrop(draggedTab, draggedTab, DragDropEffects.Move); @@ -84,35 +91,41 @@ private void StandardTabItem_PreviewMouseLeftButtonUp(object sender, MouseButton /// private void StandardTabControl_Drop(object sender, DragEventArgs e) { - TabItem? draggedTab = ViewModel.GetDraggedTab(); + var draggedTab = ViewModel.GetDraggedTab(); if (draggedTab == null || sender is not TabControl tabControl) { return; } + // Find the tab item at the drop position HitTestResult hitTestResult = VisualTreeHelper.HitTest(tabControl, e.GetPosition(tabControl)); - TabItem? targetTabItem = hitTestResult?.VisualHit != null ? FindParent(hitTestResult.VisualHit) : null; + if (hitTestResult?.VisualHit == null) + { + return; + } - if (targetTabItem != null) + // Get the target tab item from the visual tree + TabItem? targetTabItem = FindParent(hitTestResult.VisualHit); + if (targetTabItem == null) { - ViewModel.ReorderTabs(draggedTab, targetTabItem); + return; } + + // Use ViewModel to reorder tabs + ViewModel.ReorderTabs(draggedTab, targetTabItem); } - private static T? FindParent(DependencyObject child) - where T : DependencyObject + private static T? FindParent(DependencyObject child) where T : DependencyObject { - DependencyObject current = VisualTreeHelper.GetParent(child); + DependencyObject? current = child; while (current != null) { + current = VisualTreeHelper.GetParent(current); if (current is T parent) { return parent; } - - current = VisualTreeHelper.GetParent(current); } - return null; } @@ -126,17 +139,19 @@ private static bool IsCloseButton(object? source) return false; } - DependencyObject current = depObj; + DependencyObject? current = depObj; + const string closeButtonTag = "CloseButton"; + while (current != null) { - if (current is System.Windows.Controls.Button { Tag: "CloseButton" }) + if (current is System.Windows.Controls.Button button && + button.Tag is string tag && + tag == closeButtonTag) { return true; } - current = VisualTreeHelper.GetParent(current); } - return false; } } From 1dc39a04b1c913c45c7fde612bd9fba7aab7440a Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Fri, 28 Nov 2025 02:42:45 +0900 Subject: [PATCH 07/24] Custom Control Conversion Part1 --- .../Pages/Navigation/TabControlViewModel.cs | 8 +- .../Pages/Navigation/TabControlPage.xaml | 110 +--- .../Pages/Navigation/TabControlPage.xaml.cs | 128 +--- .../ApplicationAccentColorManager.cs | 2 +- .../Controls/TabControl/TabAddingEventArgs.cs | 45 ++ .../TabControl/TabClosingEventArgs.cs | 36 ++ .../Controls/TabControl/TabControl.xaml | 116 +++- .../TabControl/TabControlEventHandlers.cs | 18 + .../TabControl/TabControlExtensions.cs | 576 ++++++++++++++++++ src/Wpf.Ui/Controls/TabView/TabView.cs | 235 ++++++- src/Wpf.Ui/Controls/TabView/TabViewItem.cs | 59 +- .../TabView/TabViewItemAddingEventArgs.cs | 26 + .../TabView/TabViewItemClosingEventArgs.cs | 34 ++ 13 files changed, 1145 insertions(+), 248 deletions(-) create mode 100644 src/Wpf.Ui/Controls/TabControl/TabAddingEventArgs.cs create mode 100644 src/Wpf.Ui/Controls/TabControl/TabClosingEventArgs.cs create mode 100644 src/Wpf.Ui/Controls/TabControl/TabControlEventHandlers.cs create mode 100644 src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs create mode 100644 src/Wpf.Ui/Controls/TabView/TabViewItemAddingEventArgs.cs create mode 100644 src/Wpf.Ui/Controls/TabView/TabViewItemClosingEventArgs.cs diff --git a/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs b/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs index f0f4f6328..a953752cd 100644 --- a/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs +++ b/src/Wpf.Ui.Gallery/ViewModels/Pages/Navigation/TabControlViewModel.cs @@ -39,8 +39,8 @@ partial void OnSelectedTabChanged(TabItem? value) } [ObservableProperty] - private ObservableCollection _standardTabs = new() - { + private ObservableCollection _standardTabs = + [ new TabItem { Header = CreateTabHeader("Hello", SymbolRegular.XboxConsole24), @@ -102,7 +102,7 @@ partial void OnSelectedTabChanged(TabItem? value) Header = CreateTabHeader("tab 2", SymbolRegular.Folder24), Content = new System.Windows.Controls.TextBlock { Text = "tab 2 content", Margin = new System.Windows.Thickness(12) } }, - }; + ]; /// /// Adds a new tab to the collection. @@ -167,8 +167,6 @@ private void CloseTab(object? tabItem) /// /// Selects the specified tab and prepares for potential drag operation. /// - /// The tab item to select. - /// The starting point of the mouse operation. [RelayCommand] private void SelectTabForDrag(object? parameter) { diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml index 8cd943434..881c26d04 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml @@ -6,8 +6,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Wpf.Ui.Gallery.Views.Pages.Navigation" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" + xmlns:uiControls="clr-namespace:Wpf.Ui.Controls;assembly=Wpf.Ui" Title="TabControlPage" d:DataContext="{d:DesignInstance local:TabControlPage, IsDesignTimeCreatable=False}" @@ -23,107 +23,17 @@ Margin="0" HeaderText="Standard TabControl." XamlCode="<TabControl />"> - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs index 7700e2d88..e49f8e8b2 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs @@ -3,10 +3,7 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. -using System.Windows; using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; using Wpf.Ui.Controls; using Wpf.Ui.Gallery.ControlsLookup; using Wpf.Ui.Gallery.ViewModels.Pages.Navigation; @@ -32,126 +29,17 @@ public TabControlPage(TabControlViewModel viewModel) } } - /// - /// Handles the mouse left button down event on a tab item. - /// Selects the clicked tab and prepares for potential drag operation. - /// - private void StandardTabItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + private void OnTabClosing(object sender, TabClosingEventArgs e) { - // Don't process if the close button was clicked - if (IsCloseButton(e.OriginalSource) || sender is not TabItem tabItem) - { - return; - } - - // Use ViewModel to select the tab and prepare for drag - ViewModel.SetDragStartPoint(e.GetPosition(null)); - ViewModel.SelectTabForDragCommand.Execute(tabItem); + // The tab will be automatically removed from ItemsSource by OnTabCloseRequested + // ViewModel's CloseTabCommand is not needed here as the removal is handled automatically } - /// - /// Handles the mouse move event during drag operation. - /// Initiates drag-and-drop when the mouse has moved beyond the minimum drag distance. - /// - private void StandardTabItem_PreviewMouseMove(object sender, MouseEventArgs e) + private void OnTabAdding(object sender, TabAddingEventArgs e) { - // Don't process if the close button is being hovered or mouse button is not pressed - if (IsCloseButton(e.OriginalSource) || e.LeftButton != MouseButtonState.Pressed) - { - return; - } - - Point currentPoint = e.GetPosition(null); - - // Use ViewModel to check if drag should start - if (ViewModel.TryStartDrag(currentPoint)) - { - var draggedTab = ViewModel.GetDraggedTab(); - if (draggedTab != null) - { - DragDrop.DoDragDrop(draggedTab, draggedTab, DragDropEffects.Move); - ViewModel.EndDrag(); - } - } - } - - /// - /// Handles the mouse left button up event. - /// Clears the dragged tab reference when the drag operation ends. - /// - private void StandardTabItem_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) - { - // Use ViewModel to end drag operation - ViewModel.EndDrag(); - } - - /// - /// Handles the drop event when a tab is dropped onto the TabControl. - /// Reorders the tabs in the collection based on the drop position. - /// - private void StandardTabControl_Drop(object sender, DragEventArgs e) - { - var draggedTab = ViewModel.GetDraggedTab(); - if (draggedTab == null || sender is not TabControl tabControl) - { - return; - } - - // Find the tab item at the drop position - HitTestResult hitTestResult = VisualTreeHelper.HitTest(tabControl, e.GetPosition(tabControl)); - if (hitTestResult?.VisualHit == null) - { - return; - } - - // Get the target tab item from the visual tree - TabItem? targetTabItem = FindParent(hitTestResult.VisualHit); - if (targetTabItem == null) - { - return; - } - - // Use ViewModel to reorder tabs - ViewModel.ReorderTabs(draggedTab, targetTabItem); - } - - private static T? FindParent(DependencyObject child) where T : DependencyObject - { - DependencyObject? current = child; - while (current != null) - { - current = VisualTreeHelper.GetParent(current); - if (current is T parent) - { - return parent; - } - } - return null; - } - - /// - /// Determines if the specified element is a close button. - /// - private static bool IsCloseButton(object? source) - { - if (source is not DependencyObject depObj) - { - return false; - } - - DependencyObject? current = depObj; - const string closeButtonTag = "CloseButton"; - - while (current != null) - { - if (current is System.Windows.Controls.Button button && - button.Tag is string tag && - tag == closeButtonTag) - { - return true; - } - current = VisualTreeHelper.GetParent(current); - } - return false; + // Handle tab adding if needed + // You can customize the new tab here + // e.Header = "New Tab"; + // e.Content = new TextBlock { Text = "New Content" }; } } diff --git a/src/Wpf.Ui/Appearance/ApplicationAccentColorManager.cs b/src/Wpf.Ui/Appearance/ApplicationAccentColorManager.cs index 4da5191f3..50c92ef06 100644 --- a/src/Wpf.Ui/Appearance/ApplicationAccentColorManager.cs +++ b/src/Wpf.Ui/Appearance/ApplicationAccentColorManager.cs @@ -354,7 +354,7 @@ Color tertiaryAccent UiApplication.Current.Resources["AccentFillColorSelectedTextBackgroundBrush"] = systemAccent.ToBrush(); - var themeAccent = applicationTheme == ApplicationTheme.Dark ? secondaryAccent : primaryAccent; + Color themeAccent = applicationTheme == ApplicationTheme.Dark ? secondaryAccent : primaryAccent; UiApplication.Current.Resources["AccentFillColorDefault"] = themeAccent; UiApplication.Current.Resources["AccentFillColorDefaultBrush"] = themeAccent.ToBrush(); UiApplication.Current.Resources["AccentFillColorSecondary"] = Color.FromArgb( diff --git a/src/Wpf.Ui/Controls/TabControl/TabAddingEventArgs.cs b/src/Wpf.Ui/Controls/TabControl/TabAddingEventArgs.cs new file mode 100644 index 000000000..68f8b3416 --- /dev/null +++ b/src/Wpf.Ui/Controls/TabControl/TabAddingEventArgs.cs @@ -0,0 +1,45 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Windows; +using System.Windows.Controls; + +// ReSharper disable once CheckNamespace +namespace Wpf.Ui.Controls; + +/// +/// Provides data for the event. +/// +public class TabAddingEventArgs : RoutedEventArgs +{ + /// + /// Gets or sets the tab item to be added. If null, a new TabItem will be created. + /// + public TabItem? TabItem { get; set; } + + /// + /// Gets or sets the content for the new tab. + /// + public object? Content { get; set; } + + /// + /// Gets or sets the header for the new tab. + /// + public object? Header { get; set; } + + /// + /// Gets or sets a value indicating whether the add operation should be canceled. + /// + public bool Cancel { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public TabAddingEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + } +} + diff --git a/src/Wpf.Ui/Controls/TabControl/TabClosingEventArgs.cs b/src/Wpf.Ui/Controls/TabControl/TabClosingEventArgs.cs new file mode 100644 index 000000000..153910e74 --- /dev/null +++ b/src/Wpf.Ui/Controls/TabControl/TabClosingEventArgs.cs @@ -0,0 +1,36 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Windows; +using System.Windows.Controls; + +// ReSharper disable once CheckNamespace +namespace Wpf.Ui.Controls; + +/// +/// Provides data for the event. +/// +public class TabClosingEventArgs : RoutedEventArgs +{ + /// + /// Gets the tab item that is being closed. + /// + public TabItem TabItem { get; } + + /// + /// Gets or sets a value indicating whether the close operation should be canceled. + /// + public bool Cancel { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public TabClosingEventArgs(RoutedEvent routedEvent, TabItem tabItem) + : base(routedEvent) + { + TabItem = tabItem; + } +} + diff --git a/src/Wpf.Ui/Controls/TabControl/TabControl.xaml b/src/Wpf.Ui/Controls/TabControl/TabControl.xaml index 8c2449aba..45599ecd3 100644 --- a/src/Wpf.Ui/Controls/TabControl/TabControl.xaml +++ b/src/Wpf.Ui/Controls/TabControl/TabControl.xaml @@ -5,7 +5,7 @@ All Rights Reserved. --> - + + diff --git a/src/Wpf.Ui/Controls/TabControl/TabControlEventHandlers.cs b/src/Wpf.Ui/Controls/TabControl/TabControlEventHandlers.cs new file mode 100644 index 000000000..8780e7fd7 --- /dev/null +++ b/src/Wpf.Ui/Controls/TabControl/TabControlEventHandlers.cs @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +// ReSharper disable once CheckNamespace +namespace Wpf.Ui.Controls; + +/// +/// Represents the method that handles the event. +/// +public delegate void TabClosingEventHandler(object sender, TabClosingEventArgs e); + +/// +/// Represents the method that handles the event. +/// +public delegate void TabAddingEventHandler(object sender, TabAddingEventArgs e); + diff --git a/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs b/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs new file mode 100644 index 000000000..ca23c29a9 --- /dev/null +++ b/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs @@ -0,0 +1,576 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Collections; +using System.Collections.Specialized; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System; + +// ReSharper disable once CheckNamespace +namespace Wpf.Ui.Controls; + +/// +/// Provides attached properties for enhancing TabControl with reordering, adding, and closing capabilities. +/// +public static class TabControlExtensions +{ + private static readonly Dictionary Behaviors = new Dictionary(); + + /// Identifies the attached property. + public static readonly DependencyProperty CanReorderTabsProperty = DependencyProperty.RegisterAttached( + "CanReorderTabs", + typeof(bool), + typeof(TabControlExtensions), + new PropertyMetadata(false, OnCanReorderTabsChanged) + ); + + /// Helper for getting from . + /// CanReorderTabs property value. + [AttachedPropertyBrowsableForType(typeof(TabControl))] + public static bool GetCanReorderTabs(TabControl target) => (bool)target.GetValue(CanReorderTabsProperty); + + /// Sets the value of the attached property. + public static void SetCanReorderTabs(TabControl target, bool value) => target.SetValue(CanReorderTabsProperty, value); + + private static void OnCanReorderTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TabControl tabControl) + { + EnsureBehavior(tabControl); + } + } + + /// Identifies the attached property. + public static readonly DependencyProperty CanAddTabsProperty = DependencyProperty.RegisterAttached( + "CanAddTabs", + typeof(bool), + typeof(TabControlExtensions), + new PropertyMetadata(false, OnCanAddTabsChanged) + ); + + /// Helper for getting from . + /// CanAddTabs property value. + [AttachedPropertyBrowsableForType(typeof(TabControl))] + public static bool GetCanAddTabs(TabControl target) => (bool)target.GetValue(CanAddTabsProperty); + + /// Sets the value of the attached property. + public static void SetCanAddTabs(TabControl target, bool value) => target.SetValue(CanAddTabsProperty, value); + + private static void OnCanAddTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TabControl tabControl) + { + EnsureBehavior(tabControl); + } + } + + /// Identifies the attached property. + public static readonly DependencyProperty IsClosableProperty = DependencyProperty.RegisterAttached( + "IsClosable", + typeof(bool), + typeof(TabControlExtensions), + new PropertyMetadata(true) + ); + + /// Helper for getting from . + /// to read from. + /// IsClosable property value. + [AttachedPropertyBrowsableForType(typeof(TabItem))] + public static bool GetIsClosable(TabItem target) => (bool)target.GetValue(IsClosableProperty); + + /// + /// Sets the value of the attached property. + /// + /// to set on. + /// The value to set for the attached property. + public static void SetIsClosable(TabItem target, bool value) => target.SetValue(IsClosableProperty, value); + + /// + /// Identifies the routed event. + /// + public static readonly RoutedEvent TabClosingEvent = EventManager.RegisterRoutedEvent( + "TabClosing", + RoutingStrategy.Bubble, + typeof(TabClosingEventHandler), + typeof(TabControlExtensions) + ); + + /// + /// Adds a handler for the event. + /// + public static void AddTabClosingHandler(DependencyObject d, TabClosingEventHandler handler) + { + if (d is UIElement element) + { + element.AddHandler(TabClosingEvent, handler); + } + } + + /// + /// Removes a handler for the event. + /// + public static void RemoveTabClosingHandler(DependencyObject d, TabClosingEventHandler handler) + { + if (d is UIElement element) + { + element.RemoveHandler(TabClosingEvent, handler); + } + } + + /// + /// Identifies the routed event. + /// + public static readonly RoutedEvent TabAddingEvent = EventManager.RegisterRoutedEvent( + "TabAdding", + RoutingStrategy.Bubble, + typeof(TabAddingEventHandler), + typeof(TabControlExtensions) + ); + + /// + /// Adds a handler for the event. + /// + public static void AddTabAddingHandler(DependencyObject d, TabAddingEventHandler handler) + { + if (d is UIElement element) + { + element.AddHandler(TabAddingEvent, handler); + } + } + + /// + /// Removes a handler for the event. + /// + public static void RemoveTabAddingHandler(DependencyObject d, TabAddingEventHandler handler) + { + if (d is UIElement element) + { + element.RemoveHandler(TabAddingEvent, handler); + } + } + + private static void EnsureBehavior(TabControl tabControl) + { + if (!Behaviors.ContainsKey(tabControl)) + { + var behavior = new TabControlBehavior(tabControl); + Behaviors[tabControl] = behavior; + tabControl.Unloaded += (s, e) => + { + if (Behaviors.TryGetValue(tabControl, out TabControlBehavior? b)) + { + b.Dispose(); + Behaviors.Remove(tabControl); + } + }; + } + } + + internal static void OnTabCloseRequested(TabControl tabControl, TabItem tabItem) + { + TabClosingEventArgs args = new TabClosingEventArgs(TabClosingEvent, tabItem); + tabControl.RaiseEvent(args); + + if (!args.Cancel && GetIsClosable(tabItem)) + { + if (tabControl.ItemsSource is IList itemsSource && !itemsSource.IsReadOnly) + { + // When ItemsSource is set, remove from the bound collection + // Find the index of the TabItem container first + int containerIndex = -1; + for (int i = 0; i < tabControl.Items.Count; i++) + { + DependencyObject? container = tabControl.ItemContainerGenerator.ContainerFromIndex(i); + if (container == tabItem) + { + containerIndex = i; + break; + } + } + + // If container index found, remove from ItemsSource by index + if (containerIndex >= 0 && containerIndex < itemsSource.Count) + { + itemsSource.RemoveAt(containerIndex); + } + else + { + // Fallback: try to get the item from ItemContainerGenerator + object? item = tabControl.ItemContainerGenerator.ItemFromContainer(tabItem); + if (item == null || item == DependencyProperty.UnsetValue) + { + // Fallback to DataContext + item = tabItem.DataContext; + } + + // If still null, try to find the TabItem itself in the collection + if (item == null) + { + // For ItemsSource bound to TabItem collection, the TabItem itself might be in the collection + if (itemsSource.Contains(tabItem)) + { + item = tabItem; + } + } + + if (item != null) + { + // Try direct remove first (works for reference types) + if (itemsSource.Contains(item)) + { + itemsSource.Remove(item); + } + else + { + // Fallback: try to find by index + int index = itemsSource.IndexOf(item); + if (index >= 0) + { + itemsSource.RemoveAt(index); + } + } + } + } + } + else if (tabControl.ItemsSource == null) + { + // When ItemsSource is not set, remove from Items collection + tabControl.Items.Remove(tabItem); + } + } + } + + internal static void OnTabAddRequested(TabControl tabControl) + { + TabAddingEventArgs args = new TabAddingEventArgs(TabAddingEvent); + tabControl.RaiseEvent(args); + + if (!args.Cancel) + { + TabItem newTab = args.TabItem ?? new TabItem(); + if (args.Content != null) + { + newTab.Content = args.Content; + } + + if (args.Header != null) + { + newTab.Header = args.Header; + } + + if (tabControl.ItemsSource is IList itemsSource) + { + // When ItemsSource is set, add to the bound collection + object? itemToAdd = args.Content ?? args.Header ?? newTab; + itemsSource.Add(itemToAdd); + tabControl.SetCurrentValue(System.Windows.Controls.Primitives.Selector.SelectedItemProperty, itemToAdd); + } + else + { + // When ItemsSource is not set, add to Items collection + tabControl.Items.Add(newTab); + tabControl.SetCurrentValue(System.Windows.Controls.Primitives.Selector.SelectedItemProperty, newTab); + } + } + } + + private sealed class TabControlBehavior : IDisposable + { + private readonly TabControl _tabControl; + private TabItem? _draggedTab; + private int _draggedTabIndex = -1; + private Button? _addButton; + private Point _dragStartPoint; + private bool _isDragging; + + public TabControlBehavior(TabControl tabControl) + { + _tabControl = tabControl; + _tabControl.Loaded += OnLoaded; + if (_tabControl.IsLoaded) + { + OnLoaded(_tabControl, new RoutedEventArgs()); + } + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + UpdateTabItems(); + SetupAddButton(); + if (_tabControl.Items is INotifyCollectionChanged notifyCollection) + { + notifyCollection.CollectionChanged += OnItemsCollectionChanged; + } + } + + private void SetupAddButton() + { + _tabControl.ApplyTemplate(); + if (_tabControl.Template?.FindName("AddButton", _tabControl) is Button addButton) + { + _addButton = addButton; + addButton.Click -= OnAddButtonClick; + addButton.Click += OnAddButtonClick; + } + } + + private void OnAddButtonClick(object sender, RoutedEventArgs e) + { + OnTabAddRequested(_tabControl); + } + + private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + UpdateTabItems(); + } + + private void UpdateTabItems() + { + // When ItemsSource is set, Items collection contains TabItem containers + // We need to get the TabItem containers from the ItemContainerGenerator + if (_tabControl.ItemsSource != null) + { + for (int i = 0; i < _tabControl.Items.Count; i++) + { + if (_tabControl.ItemContainerGenerator.ContainerFromIndex(i) is TabItem tabItem) + { + SetupTabItem(tabItem); + } + } + } + else + { + foreach (object? item in _tabControl.Items) + { + if (item is TabItem tabItem) + { + SetupTabItem(tabItem); + } + } + } + } + + private void SetupTabItem(TabItem tabItem) + { + if (GetCanReorderTabs(_tabControl)) + { + tabItem.PreviewMouseLeftButtonDown -= OnTabItemPreviewMouseLeftButtonDown; + tabItem.PreviewMouseMove -= OnTabItemPreviewMouseMove; + tabItem.PreviewMouseLeftButtonUp -= OnTabItemPreviewMouseLeftButtonUp; + tabItem.PreviewMouseLeftButtonDown += OnTabItemPreviewMouseLeftButtonDown; + tabItem.PreviewMouseMove += OnTabItemPreviewMouseMove; + tabItem.PreviewMouseLeftButtonUp += OnTabItemPreviewMouseLeftButtonUp; + } + + // Setup close button + tabItem.Loaded += OnTabItemLoaded; + if (tabItem.IsLoaded) + { + OnTabItemLoaded(tabItem, new RoutedEventArgs()); + } + } + + private void OnTabItemLoaded(object sender, RoutedEventArgs e) + { + if (sender is TabItem tabItem) + { + tabItem.ApplyTemplate(); + if (tabItem.Template?.FindName("CloseButton", tabItem) is Button closeButton) + { + // Remove previous handlers + closeButton.Click -= OnCloseButtonClick; + closeButton.PreviewMouseLeftButtonDown -= OnCloseButtonPreviewMouseLeftButtonDown; + + // Add handlers + closeButton.Click += OnCloseButtonClick; + closeButton.PreviewMouseLeftButtonDown += OnCloseButtonPreviewMouseLeftButtonDown; + } + } + } + + private void OnCloseButtonPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + // Mark the event as handled to prevent it from bubbling up to the TabItem + e.Handled = true; + } + + private void OnCloseButtonClick(object sender, RoutedEventArgs e) + { + // Mark the event as handled to prevent it from bubbling up to the TabItem + e.Handled = true; + + if (sender is Button button) + { + // Try to get TabItem from TemplatedParent first + TabItem? tabItem = button.TemplatedParent as TabItem; + + if (tabItem == null) + { + // If TemplatedParent is not available, find the TabItem in the visual tree + DependencyObject? current = button; + while (current != null) + { + current = VisualTreeHelper.GetParent(current); + if (current is TabItem item) + { + tabItem = item; + break; + } + } + } + + if (tabItem != null) + { + OnTabCloseRequested(_tabControl, tabItem); + } + } + } + + private void OnTabItemPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (sender is TabItem tabItem && GetCanReorderTabs(_tabControl)) + { + // Check if the click is on the close button or any of its children + if (IsCloseButton(e.OriginalSource)) + { + // Don't start drag operation if clicking on close button + e.Handled = false; // Let the close button handle the event + return; + } + + _draggedTab = tabItem; + _draggedTabIndex = _tabControl.Items.IndexOf(tabItem); + _dragStartPoint = e.GetPosition(null); + _isDragging = false; + tabItem.CaptureMouse(); + } + } + + private static bool IsCloseButton(object? source) + { + if (source is not DependencyObject depObj) + { + return false; + } + + DependencyObject? current = depObj; + while (current != null) + { + if (current is Button button && button.Name == "CloseButton") + { + return true; + } + + current = VisualTreeHelper.GetParent(current); + } + + return false; + } + + private void OnTabItemPreviewMouseMove(object sender, MouseEventArgs e) + { + if (_draggedTab != null && e.LeftButton == MouseButtonState.Pressed && GetCanReorderTabs(_tabControl)) + { + Point currentPosition = e.GetPosition(null); + + // Check if the mouse has moved far enough to start dragging + if (!_isDragging) + { + double deltaX = currentPosition.X - _dragStartPoint.X; + double deltaY = currentPosition.Y - _dragStartPoint.Y; + double minDistance = SystemParameters.MinimumHorizontalDragDistance; + + if (Math.Abs(deltaX) < minDistance && Math.Abs(deltaY) < minDistance) + { + // Not enough movement to start dragging + return; + } + + _isDragging = true; + } + + Point tabControlPosition = e.GetPosition(_tabControl); + TabItem? tabItem = GetTabItemAtPosition(tabControlPosition); + + if (tabItem != null && tabItem != _draggedTab) + { + int newIndex = _tabControl.Items.IndexOf(tabItem); + if (newIndex >= 0 && newIndex != _draggedTabIndex) + { + ReorderTabItem(_draggedTabIndex, newIndex); + _draggedTabIndex = newIndex; + _tabControl.SetCurrentValue(System.Windows.Controls.Primitives.Selector.SelectedItemProperty, _draggedTab); + } + } + } + } + + private void ReorderTabItem(int oldIndex, int newIndex) + { + if (_tabControl.ItemsSource is IList itemsSource && !itemsSource.IsReadOnly) + { + // When ItemsSource is set, operate on the bound collection + object? item = itemsSource[oldIndex]; + itemsSource.RemoveAt(oldIndex); + itemsSource.Insert(newIndex, item); + } + else if (_tabControl.ItemsSource == null) + { + // When ItemsSource is not set, operate on Items collection + object? item = _tabControl.Items[oldIndex]; + _tabControl.Items.RemoveAt(oldIndex); + _tabControl.Items.Insert(newIndex, item); + } + } + + private void OnTabItemPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (_draggedTab != null) + { + _draggedTab.ReleaseMouseCapture(); + _draggedTab = null; + _draggedTabIndex = -1; + _isDragging = false; + } + } + + private TabItem? GetTabItemAtPosition(Point position) + { + HitTestResult? hitTestResult = VisualTreeHelper.HitTest(_tabControl, position); + if (hitTestResult?.VisualHit != null) + { + DependencyObject? current = hitTestResult.VisualHit; + while (current != null) + { + if (current is TabItem tabItem) + { + return tabItem; + } + + current = VisualTreeHelper.GetParent(current) as Visual; + } + } + + return null; + } + + public void Dispose() + { + if (_addButton != null) + { + _addButton.Click -= OnAddButtonClick; + } + + if (_tabControl.Items is INotifyCollectionChanged notifyCollection) + { + notifyCollection.CollectionChanged -= OnItemsCollectionChanged; + } + } + } +} + diff --git a/src/Wpf.Ui/Controls/TabView/TabView.cs b/src/Wpf.Ui/Controls/TabView/TabView.cs index b3a5af2e5..247184e3e 100644 --- a/src/Wpf.Ui/Controls/TabView/TabView.cs +++ b/src/Wpf.Ui/Controls/TabView/TabView.cs @@ -3,6 +3,13 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +using System.Collections; +using System.Collections.Specialized; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + // ReSharper disable once CheckNamespace namespace Wpf.Ui.Controls; @@ -11,4 +18,230 @@ namespace Wpf.Ui.Controls; /// Tab controls are useful for displaying several pages (or documents) of content while /// giving a user the capability to rearrange, open, or close new tabs. /// -public class TabView : System.Windows.Controls.TabControl { } +[TemplatePart(Name = "PART_AddButton", Type = typeof(System.Windows.Controls.Button))] +public class TabView : System.Windows.Controls.TabControl +{ + private TabViewItem? _draggedTab; + private int _draggedTabIndex = -1; + + /// Identifies the dependency property. + public static readonly DependencyProperty CanReorderTabsProperty = DependencyProperty.Register( + nameof(CanReorderTabs), + typeof(bool), + typeof(TabView), + new PropertyMetadata(true) + ); + + /// Identifies the dependency property. + public static readonly DependencyProperty CanAddTabsProperty = DependencyProperty.Register( + nameof(CanAddTabs), + typeof(bool), + typeof(TabView), + new PropertyMetadata(true) + ); + + /// + /// Gets or sets a value indicating whether tabs can be reordered by dragging. + /// + public bool CanReorderTabs + { + get => (bool)GetValue(CanReorderTabsProperty); + set => SetValue(CanReorderTabsProperty, value); + } + + /// + /// Gets or sets a value indicating whether new tabs can be added. + /// + public bool CanAddTabs + { + get => (bool)GetValue(CanAddTabsProperty); + set => SetValue(CanAddTabsProperty, value); + } + + /// + /// Occurs when a tab is requested to be closed. + /// + public event EventHandler? TabClosing; + + /// + /// Occurs when a new tab is requested to be added. + /// + public event EventHandler? TabAdding; + + static TabView() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(TabView), new FrameworkPropertyMetadata(typeof(TabView))); + } + + public TabView() + { + Loaded += OnLoaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + Loaded -= OnLoaded; + UpdateTabItems(); + if (Items is INotifyCollectionChanged notifyCollection) + { + notifyCollection.CollectionChanged += OnItemsCollectionChanged; + } + } + + private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + UpdateTabItems(); + } + + private void UpdateTabItems() + { + foreach (object? item in Items) + { + if (item is TabViewItem tabItem) + { + SetupTabItem(tabItem); + } + else if (item is FrameworkElement element && element.Parent is TabViewItem tabItem2) + { + SetupTabItem(tabItem2); + } + } + } + + private void SetupTabItem(TabViewItem tabItem) + { + tabItem.CloseRequested -= OnTabCloseRequested; + tabItem.CloseRequested += OnTabCloseRequested; + + if (CanReorderTabs) + { + tabItem.PreviewMouseLeftButtonDown -= OnTabItemPreviewMouseLeftButtonDown; + tabItem.PreviewMouseMove -= OnTabItemPreviewMouseMove; + tabItem.PreviewMouseLeftButtonUp -= OnTabItemPreviewMouseLeftButtonUp; + tabItem.PreviewMouseLeftButtonDown += OnTabItemPreviewMouseLeftButtonDown; + tabItem.PreviewMouseMove += OnTabItemPreviewMouseMove; + tabItem.PreviewMouseLeftButtonUp += OnTabItemPreviewMouseLeftButtonUp; + } + } + + private void OnTabCloseRequested(object sender, RoutedEventArgs e) + { + if (sender is TabViewItem tabItem) + { + TabViewItemClosingEventArgs args = new TabViewItemClosingEventArgs(tabItem); + TabClosing?.Invoke(this, args); + + if (!args.Cancel && tabItem.IsClosable) + { + Items.Remove(tabItem); + } + } + } + + private void OnTabItemPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (sender is TabViewItem tabItem && CanReorderTabs) + { + _draggedTab = tabItem; + _draggedTabIndex = Items.IndexOf(tabItem); + tabItem.CaptureMouse(); + } + } + + private void OnTabItemPreviewMouseMove(object sender, MouseEventArgs e) + { + if (_draggedTab != null && e.LeftButton == MouseButtonState.Pressed && CanReorderTabs) + { + Point currentPosition = e.GetPosition(this); + TabViewItem? tabItem = GetTabItemAtPosition(currentPosition); + + if (tabItem != null && tabItem != _draggedTab) + { + int newIndex = Items.IndexOf(tabItem); + if (newIndex >= 0 && newIndex != _draggedTabIndex) + { + Items.RemoveAt(_draggedTabIndex); + Items.Insert(newIndex, _draggedTab); + _draggedTabIndex = newIndex; + SetCurrentValue(SelectedItemProperty, _draggedTab); + } + } + } + } + + private void OnTabItemPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (_draggedTab != null) + { + _draggedTab.ReleaseMouseCapture(); + _draggedTab = null; + _draggedTabIndex = -1; + } + } + + private TabViewItem? GetTabItemAtPosition(Point position) + { + HitTestResult hitTestResult = VisualTreeHelper.HitTest(this, position); + if (hitTestResult?.VisualHit != null) + { + DependencyObject? current = hitTestResult.VisualHit; + while (current != null) + { + if (current is TabViewItem tabItem) + { + return tabItem; + } + + current = VisualTreeHelper.GetParent(current) as Visual; + } + } + + return null; + } + + /// + /// Adds a new tab to the TabView. + /// + public void AddTab(object? content = null, string? header = null) + { + TabViewItemAddingEventArgs args = new TabViewItemAddingEventArgs(); + TabAdding?.Invoke(this, args); + + if (args.Cancel) + { + return; + } + + TabViewItem newTab = args.TabItem ?? new TabViewItem(); + if (content != null) + { + newTab.Content = content; + } + + if (header != null) + { + newTab.Header = header; + } + + Items.Add(newTab); + SetCurrentValue(SelectedItemProperty, newTab); + SetupTabItem(newTab); + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + if (GetTemplateChild("PART_AddButton") is System.Windows.Controls.Button addButton) + { + addButton.Click -= OnAddButtonClick; + addButton.Click += OnAddButtonClick; + } + } + + private void OnAddButtonClick(object sender, RoutedEventArgs e) + { + AddTab(); + } +} + diff --git a/src/Wpf.Ui/Controls/TabView/TabViewItem.cs b/src/Wpf.Ui/Controls/TabView/TabViewItem.cs index cfe878440..d01b1c72b 100644 --- a/src/Wpf.Ui/Controls/TabView/TabViewItem.cs +++ b/src/Wpf.Ui/Controls/TabView/TabViewItem.cs @@ -3,10 +3,67 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +using System.Windows; +using System.Windows.Input; + // ReSharper disable once CheckNamespace namespace Wpf.Ui.Controls; /// /// Represents a single tab within a . /// -public class TabViewItem : System.Windows.Controls.TabItem { } +[TemplatePart(Name = "PART_CloseButton", Type = typeof(System.Windows.Controls.Button))] +public class TabViewItem : System.Windows.Controls.TabItem +{ + /// Identifies the dependency property. + public static readonly DependencyProperty IsClosableProperty = DependencyProperty.Register( + nameof(IsClosable), + typeof(bool), + typeof(TabViewItem), + new PropertyMetadata(true) + ); + + /// + /// Gets or sets a value indicating whether the tab can be closed. + /// + public bool IsClosable + { + get => (bool)GetValue(IsClosableProperty); + set => SetValue(IsClosableProperty, value); + } + + /// + /// Occurs when the close button is clicked. + /// + public event RoutedEventHandler? CloseRequested; + + /// + /// Raises the event. + /// + internal void OnCloseRequested() + { + CloseRequested?.Invoke(this, new RoutedEventArgs()); + } + + static TabViewItem() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(TabViewItem), new FrameworkPropertyMetadata(typeof(TabViewItem))); + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + if (GetTemplateChild("PART_CloseButton") is System.Windows.Controls.Button closeButton) + { + closeButton.Click -= OnCloseButtonClick; + closeButton.Click += OnCloseButtonClick; + } + } + + private void OnCloseButtonClick(object sender, RoutedEventArgs e) + { + e.Handled = true; + OnCloseRequested(); + } +} diff --git a/src/Wpf.Ui/Controls/TabView/TabViewItemAddingEventArgs.cs b/src/Wpf.Ui/Controls/TabView/TabViewItemAddingEventArgs.cs new file mode 100644 index 000000000..015eddd1b --- /dev/null +++ b/src/Wpf.Ui/Controls/TabView/TabViewItemAddingEventArgs.cs @@ -0,0 +1,26 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Windows; +using System.Windows.Input; + +// ReSharper disable once CheckNamespace +namespace Wpf.Ui.Controls; + +/// +/// Provides data for the event. +/// +public class TabViewItemAddingEventArgs : EventArgs +{ + /// + /// Gets or sets the tab item to be added. If null, a new TabViewItem will be created. + /// + public TabViewItem? TabItem { get; set; } + + /// + /// Gets or sets a value indicating whether the add operation should be canceled. + /// + public bool Cancel { get; set; } +} diff --git a/src/Wpf.Ui/Controls/TabView/TabViewItemClosingEventArgs.cs b/src/Wpf.Ui/Controls/TabView/TabViewItemClosingEventArgs.cs new file mode 100644 index 000000000..f4cfe9df5 --- /dev/null +++ b/src/Wpf.Ui/Controls/TabView/TabViewItemClosingEventArgs.cs @@ -0,0 +1,34 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Windows; +using System.Windows.Input; + +// ReSharper disable once CheckNamespace +namespace Wpf.Ui.Controls; + +/// +/// Provides data for the event. +/// +public class TabViewItemClosingEventArgs : EventArgs +{ + /// + /// Gets the tab item that is being closed. + /// + public TabViewItem TabItem { get; } + + /// + /// Gets or sets a value indicating whether the close operation should be canceled. + /// + public bool Cancel { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public TabViewItemClosingEventArgs(TabViewItem tabItem) + { + TabItem = tabItem; + } +} \ No newline at end of file From 275cec6369bc70bbd9aed3daf1878de87f209200 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Fri, 28 Nov 2025 05:52:41 +0900 Subject: [PATCH 08/24] Implementation of the tab close button --- .../TabControl/TabControlExtensions.cs | 207 +++++++++++++----- 1 file changed, 147 insertions(+), 60 deletions(-) diff --git a/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs b/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs index ca23c29a9..c08507963 100644 --- a/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs +++ b/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs @@ -3,13 +3,13 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +using System; using System.Collections; using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; -using System; // ReSharper disable once CheckNamespace namespace Wpf.Ui.Controls; @@ -181,59 +181,32 @@ internal static void OnTabCloseRequested(TabControl tabControl, TabItem tabItem) if (tabControl.ItemsSource is IList itemsSource && !itemsSource.IsReadOnly) { // When ItemsSource is set, remove from the bound collection - // Find the index of the TabItem container first - int containerIndex = -1; - for (int i = 0; i < tabControl.Items.Count; i++) + // Get the data item from ItemContainerGenerator (for ItemsSource bound to TabItem collection, this returns the TabItem itself) + object? item = tabControl.ItemContainerGenerator.ItemFromContainer(tabItem); + if (item == null || item == DependencyProperty.UnsetValue) { - DependencyObject? container = tabControl.ItemContainerGenerator.ContainerFromIndex(i); - if (container == tabItem) - { - containerIndex = i; - break; - } + // Fallback: try DataContext + item = tabItem.DataContext; } - // If container index found, remove from ItemsSource by index - if (containerIndex >= 0 && containerIndex < itemsSource.Count) + // If still null, the TabItem itself might be in the collection (for ItemsSource bound to TabItem collection) + if (item == null) { - itemsSource.RemoveAt(containerIndex); + item = tabItem; } - else + + // Remove the item from the collection (similar to TabControlViewModel.CloseTab) + if (item != null) { - // Fallback: try to get the item from ItemContainerGenerator - object? item = tabControl.ItemContainerGenerator.ItemFromContainer(tabItem); - if (item == null || item == DependencyProperty.UnsetValue) + int index = itemsSource.IndexOf(item); + if (index >= 0) { - // Fallback to DataContext - item = tabItem.DataContext; + itemsSource.RemoveAt(index); } - - // If still null, try to find the TabItem itself in the collection - if (item == null) + else { - // For ItemsSource bound to TabItem collection, the TabItem itself might be in the collection - if (itemsSource.Contains(tabItem)) - { - item = tabItem; - } - } - - if (item != null) - { - // Try direct remove first (works for reference types) - if (itemsSource.Contains(item)) - { - itemsSource.Remove(item); - } - else - { - // Fallback: try to find by index - int index = itemsSource.IndexOf(item); - if (index >= 0) - { - itemsSource.RemoveAt(index); - } - } + // Fallback: try direct remove + itemsSource.Remove(item); } } } @@ -335,6 +308,14 @@ private void UpdateTabItems() // We need to get the TabItem containers from the ItemContainerGenerator if (_tabControl.ItemsSource != null) { + // Wait for containers to be generated + if (_tabControl.ItemContainerGenerator.Status != System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated) + { + // If containers are not ready, wait for them + _tabControl.ItemContainerGenerator.StatusChanged += OnItemContainerGeneratorStatusChanged; + return; + } + for (int i = 0; i < _tabControl.Items.Count; i++) { if (_tabControl.ItemContainerGenerator.ContainerFromIndex(i) is TabItem tabItem) @@ -355,6 +336,15 @@ private void UpdateTabItems() } } + private void OnItemContainerGeneratorStatusChanged(object? sender, EventArgs e) + { + if (_tabControl.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated) + { + _tabControl.ItemContainerGenerator.StatusChanged -= OnItemContainerGeneratorStatusChanged; + UpdateTabItems(); + } + } + private void SetupTabItem(TabItem tabItem) { if (GetCanReorderTabs(_tabControl)) @@ -367,11 +357,18 @@ private void SetupTabItem(TabItem tabItem) tabItem.PreviewMouseLeftButtonUp += OnTabItemPreviewMouseLeftButtonUp; } - // Setup close button + // Setup close button - use Loaded event and also try immediately + tabItem.Loaded -= OnTabItemLoaded; tabItem.Loaded += OnTabItemLoaded; + + // Try to setup immediately if already loaded if (tabItem.IsLoaded) { - OnTabItemLoaded(tabItem, new RoutedEventArgs()); + // Use Dispatcher to ensure template is fully applied + tabItem.Dispatcher.BeginInvoke(new Action(() => + { + SetupCloseButton(tabItem); + }), System.Windows.Threading.DispatcherPriority.Loaded); } } @@ -379,36 +376,126 @@ private void OnTabItemLoaded(object sender, RoutedEventArgs e) { if (sender is TabItem tabItem) { - tabItem.ApplyTemplate(); - if (tabItem.Template?.FindName("CloseButton", tabItem) is Button closeButton) + // Use Dispatcher to ensure template is fully applied and rendered + tabItem.Dispatcher.BeginInvoke(new Action(() => { - // Remove previous handlers - closeButton.Click -= OnCloseButtonClick; - closeButton.PreviewMouseLeftButtonDown -= OnCloseButtonPreviewMouseLeftButtonDown; - - // Add handlers - closeButton.Click += OnCloseButtonClick; - closeButton.PreviewMouseLeftButtonDown += OnCloseButtonPreviewMouseLeftButtonDown; + SetupCloseButton(tabItem); + }), System.Windows.Threading.DispatcherPriority.Loaded); + } + } + + private void SetupCloseButton(TabItem tabItem) + { + // Ensure template is applied + tabItem.ApplyTemplate(); + + // Try to find CloseButton - use multiple attempts to ensure it's found + System.Windows.Controls.Button? closeButton = null; + + // First try: FindName (most reliable for template elements) + object? foundElement = tabItem.Template?.FindName("CloseButton", tabItem); + closeButton = foundElement as System.Windows.Controls.Button; + + // Second try: Visual tree search by position (Grid.Column="1") if FindName fails + if (closeButton == null) + { + closeButton = FindCloseButtonInVisualTree(tabItem); + } + + if (closeButton != null) + { + // Remove previous handlers + closeButton.Click -= OnCloseButtonClick; + closeButton.PreviewMouseLeftButtonDown -= OnCloseButtonPreviewMouseLeftButtonDown; + + // Add handlers + closeButton.Click += OnCloseButtonClick; + closeButton.PreviewMouseLeftButtonDown += OnCloseButtonPreviewMouseLeftButtonDown; + } + } + + private static System.Windows.Controls.Button? FindCloseButtonInVisualTree(DependencyObject parent) + { + int childCount = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < childCount; i++) + { + DependencyObject? child = VisualTreeHelper.GetChild(parent, i); + if (child is System.Windows.Controls.Button button) + { + // Check if it's in the second column of a Grid (CloseButton is in Grid.Column="1") + // This is more reliable than Name property which may not be set + int column = Grid.GetColumn(button); + if (column == 1) + { + // This is likely the CloseButton based on position + return button; + } + + // Also check Name property as fallback + string? name = button.Name; + if (name == "CloseButton") + { + return button; + } + } + + if (child != null) + { + System.Windows.Controls.Button? found = FindCloseButtonInVisualTree(child); + if (found != null) + { + return found; + } } } + + return null; } private void OnCloseButtonPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // Mark the event as handled to prevent it from bubbling up to the TabItem + // and handle the close action directly here since Click event may not fire e.Handled = true; + + if (sender is System.Windows.Controls.Button button) + { + // Try to get TabItem from TemplatedParent first + TabItem? tabItem = button.TemplatedParent as TabItem; + + if (tabItem == null) + { + // If TemplatedParent is not available, find the TabItem in the visual tree + DependencyObject? current = button; + while (current != null) + { + current = VisualTreeHelper.GetParent(current); + if (current is TabItem item) + { + tabItem = item; + break; + } + } + } + + if (tabItem != null) + { + OnTabCloseRequested(_tabControl, tabItem); + } + } } private void OnCloseButtonClick(object sender, RoutedEventArgs e) { // Mark the event as handled to prevent it from bubbling up to the TabItem e.Handled = true; - - if (sender is Button button) + + // sender is System.Windows.Controls.Button + if (sender is System.Windows.Controls.Button button) { // Try to get TabItem from TemplatedParent first TabItem? tabItem = button.TemplatedParent as TabItem; - + if (tabItem == null) { // If TemplatedParent is not available, find the TabItem in the visual tree @@ -439,7 +526,7 @@ private void OnTabItemPreviewMouseLeftButtonDown(object sender, MouseButtonEvent if (IsCloseButton(e.OriginalSource)) { // Don't start drag operation if clicking on close button - e.Handled = false; // Let the close button handle the event + // Let the close button handle the event return; } From 4349491da67531cfcf3a35788a409fc7aca91d21 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Fri, 28 Nov 2025 06:16:28 +0900 Subject: [PATCH 09/24] Optimization --- .../TabControl/TabControlExtensions.cs | 181 ++++++++++-------- 1 file changed, 98 insertions(+), 83 deletions(-) diff --git a/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs b/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs index c08507963..61ee73370 100644 --- a/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs +++ b/src/Wpf.Ui/Controls/TabControl/TabControlExtensions.cs @@ -156,19 +156,21 @@ public static void RemoveTabAddingHandler(DependencyObject d, TabAddingEventHand private static void EnsureBehavior(TabControl tabControl) { - if (!Behaviors.ContainsKey(tabControl)) + if (Behaviors.ContainsKey(tabControl)) { - var behavior = new TabControlBehavior(tabControl); - Behaviors[tabControl] = behavior; - tabControl.Unloaded += (s, e) => - { - if (Behaviors.TryGetValue(tabControl, out TabControlBehavior? b)) - { - b.Dispose(); - Behaviors.Remove(tabControl); - } - }; + return; } + + TabControlBehavior behavior = new TabControlBehavior(tabControl); + Behaviors[tabControl] = behavior; + tabControl.Unloaded += (s, e) => + { + if (Behaviors.TryGetValue(tabControl, out TabControlBehavior? b)) + { + b.Dispose(); + Behaviors.Remove(tabControl); + } + }; } internal static void OnTabCloseRequested(TabControl tabControl, TabItem tabItem) @@ -257,7 +259,7 @@ private sealed class TabControlBehavior : IDisposable private readonly TabControl _tabControl; private TabItem? _draggedTab; private int _draggedTabIndex = -1; - private Button? _addButton; + private System.Windows.Controls.Button? _addButton; private Point _dragStartPoint; private bool _isDragging; @@ -284,7 +286,7 @@ private void OnLoaded(object sender, RoutedEventArgs e) private void SetupAddButton() { _tabControl.ApplyTemplate(); - if (_tabControl.Template?.FindName("AddButton", _tabControl) is Button addButton) + if (_tabControl.Template?.FindName("AddButton", _tabControl) is System.Windows.Controls.Button addButton) { _addButton = addButton; addButton.Click -= OnAddButtonClick; @@ -299,7 +301,14 @@ private void OnAddButtonClick(object sender, RoutedEventArgs e) private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - UpdateTabItems(); + // Only update if items were actually added or removed + if (e.Action == NotifyCollectionChangedAction.Add || + e.Action == NotifyCollectionChangedAction.Remove || + e.Action == NotifyCollectionChangedAction.Replace || + e.Action == NotifyCollectionChangedAction.Reset) + { + UpdateTabItems(); + } } private void UpdateTabItems() @@ -343,6 +352,11 @@ private void OnItemContainerGeneratorStatusChanged(object? sender, EventArgs e) _tabControl.ItemContainerGenerator.StatusChanged -= OnItemContainerGeneratorStatusChanged; UpdateTabItems(); } + else if (_tabControl.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.Error) + { + // Unsubscribe on error to avoid memory leaks + _tabControl.ItemContainerGenerator.StatusChanged -= OnItemContainerGeneratorStatusChanged; + } } private void SetupTabItem(TabItem tabItem) @@ -365,10 +379,9 @@ private void SetupTabItem(TabItem tabItem) if (tabItem.IsLoaded) { // Use Dispatcher to ensure template is fully applied - tabItem.Dispatcher.BeginInvoke(new Action(() => - { - SetupCloseButton(tabItem); - }), System.Windows.Threading.DispatcherPriority.Loaded); + tabItem.Dispatcher.BeginInvoke( + () => SetupCloseButton(tabItem), + System.Windows.Threading.DispatcherPriority.Loaded); } } @@ -377,10 +390,9 @@ private void OnTabItemLoaded(object sender, RoutedEventArgs e) if (sender is TabItem tabItem) { // Use Dispatcher to ensure template is fully applied and rendered - tabItem.Dispatcher.BeginInvoke(new Action(() => - { - SetupCloseButton(tabItem); - }), System.Windows.Threading.DispatcherPriority.Loaded); + tabItem.Dispatcher.BeginInvoke( + () => SetupCloseButton(tabItem), + System.Windows.Threading.DispatcherPriority.Loaded); } } @@ -390,21 +402,14 @@ private void SetupCloseButton(TabItem tabItem) tabItem.ApplyTemplate(); // Try to find CloseButton - use multiple attempts to ensure it's found - System.Windows.Controls.Button? closeButton = null; - - // First try: FindName (most reliable for template elements) - object? foundElement = tabItem.Template?.FindName("CloseButton", tabItem); - closeButton = foundElement as System.Windows.Controls.Button; + System.Windows.Controls.Button? closeButton = tabItem.Template?.FindName("CloseButton", tabItem) as System.Windows.Controls.Button; - // Second try: Visual tree search by position (Grid.Column="1") if FindName fails - if (closeButton == null) - { - closeButton = FindCloseButtonInVisualTree(tabItem); - } + // Fallback: Visual tree search by position (Grid.Column="1") if FindName fails + closeButton ??= FindCloseButtonInVisualTree(tabItem); if (closeButton != null) { - // Remove previous handlers + // Remove previous handlers to avoid duplicate subscriptions closeButton.Click -= OnCloseButtonClick; closeButton.PreviewMouseLeftButtonDown -= OnCloseButtonPreviewMouseLeftButtonDown; @@ -452,32 +457,36 @@ private void SetupCloseButton(TabItem tabItem) return null; } + private static TabItem? FindTabItemFromButton(System.Windows.Controls.Button button) + { + // Try to get TabItem from TemplatedParent first + if (button.TemplatedParent is TabItem tabItem) + { + return tabItem; + } + + // If TemplatedParent is not available, find the TabItem in the visual tree + DependencyObject? current = button; + while (current != null) + { + current = VisualTreeHelper.GetParent(current); + if (current is TabItem item) + { + return item; + } + } + + return null; + } + private void OnCloseButtonPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // Mark the event as handled to prevent it from bubbling up to the TabItem - // and handle the close action directly here since Click event may not fire e.Handled = true; if (sender is System.Windows.Controls.Button button) { - // Try to get TabItem from TemplatedParent first - TabItem? tabItem = button.TemplatedParent as TabItem; - - if (tabItem == null) - { - // If TemplatedParent is not available, find the TabItem in the visual tree - DependencyObject? current = button; - while (current != null) - { - current = VisualTreeHelper.GetParent(current); - if (current is TabItem item) - { - tabItem = item; - break; - } - } - } - + TabItem? tabItem = FindTabItemFromButton(button); if (tabItem != null) { OnTabCloseRequested(_tabControl, tabItem); @@ -490,27 +499,9 @@ private void OnCloseButtonClick(object sender, RoutedEventArgs e) // Mark the event as handled to prevent it from bubbling up to the TabItem e.Handled = true; - // sender is System.Windows.Controls.Button if (sender is System.Windows.Controls.Button button) { - // Try to get TabItem from TemplatedParent first - TabItem? tabItem = button.TemplatedParent as TabItem; - - if (tabItem == null) - { - // If TemplatedParent is not available, find the TabItem in the visual tree - DependencyObject? current = button; - while (current != null) - { - current = VisualTreeHelper.GetParent(current); - if (current is TabItem item) - { - tabItem = item; - break; - } - } - } - + TabItem? tabItem = FindTabItemFromButton(button); if (tabItem != null) { OnTabCloseRequested(_tabControl, tabItem); @@ -548,9 +539,19 @@ private static bool IsCloseButton(object? source) DependencyObject? current = depObj; while (current != null) { - if (current is Button button && button.Name == "CloseButton") + if (current is System.Windows.Controls.Button button) { - return true; + // Check by Grid column position (more reliable than Name) + if (Grid.GetColumn(button) == 1) + { + return true; + } + + // Fallback: check by Name + if (button.Name == "CloseButton") + { + return true; + } } current = VisualTreeHelper.GetParent(current); @@ -599,19 +600,31 @@ private void OnTabItemPreviewMouseMove(object sender, MouseEventArgs e) private void ReorderTabItem(int oldIndex, int newIndex) { + // Validate indices + if (oldIndex < 0 || newIndex < 0 || oldIndex == newIndex) + { + return; + } + if (_tabControl.ItemsSource is IList itemsSource && !itemsSource.IsReadOnly) { // When ItemsSource is set, operate on the bound collection - object? item = itemsSource[oldIndex]; - itemsSource.RemoveAt(oldIndex); - itemsSource.Insert(newIndex, item); + if (oldIndex < itemsSource.Count && newIndex < itemsSource.Count) + { + object? item = itemsSource[oldIndex]; + itemsSource.RemoveAt(oldIndex); + itemsSource.Insert(newIndex, item); + } } else if (_tabControl.ItemsSource == null) { // When ItemsSource is not set, operate on Items collection - object? item = _tabControl.Items[oldIndex]; - _tabControl.Items.RemoveAt(oldIndex); - _tabControl.Items.Insert(newIndex, item); + if (oldIndex < _tabControl.Items.Count && newIndex < _tabControl.Items.Count) + { + object? item = _tabControl.Items[oldIndex]; + _tabControl.Items.RemoveAt(oldIndex); + _tabControl.Items.Insert(newIndex, item); + } } } @@ -648,15 +661,17 @@ private void OnTabItemPreviewMouseLeftButtonUp(object sender, MouseButtonEventAr public void Dispose() { - if (_addButton != null) - { - _addButton.Click -= OnAddButtonClick; - } + // Clean up event handlers + _addButton?.Click -= OnAddButtonClick; if (_tabControl.Items is INotifyCollectionChanged notifyCollection) { notifyCollection.CollectionChanged -= OnItemsCollectionChanged; } + + // Clean up dragged tab reference + _draggedTab?.ReleaseMouseCapture(); + _draggedTab = null; } } } From 1a6422b20251355a906f3ebda18ea9d1b31c2620 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Fri, 28 Nov 2025 23:51:57 +0900 Subject: [PATCH 10/24] Add New Tab --- .../Pages/Navigation/TabControlPage.xaml.cs | 20 +++++++-- .../Controls/TabControl/TabControl.xaml | 7 +++- .../TabControl/TabControlExtensions.cs | 41 +++++++++++++++---- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs index e49f8e8b2..409ae63e1 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs +++ b/src/Wpf.Ui.Gallery/Views/Pages/Navigation/TabControlPage.xaml.cs @@ -37,9 +37,21 @@ private void OnTabClosing(object sender, TabClosingEventArgs e) private void OnTabAdding(object sender, TabAddingEventArgs e) { - // Handle tab adding if needed - // You can customize the new tab here - // e.Header = "New Tab"; - // e.Content = new TextBlock { Text = "New Content" }; + // Method 1: Set tab name using TabAddingEventArgs Header property + // Get the tab number (get current tab count from ViewModel) + int tabNumber = ViewModel.StandardTabs.Count + 1; + e.Header = $"New Tab {tabNumber}"; + e.Content = new System.Windows.Controls.TextBlock + { + Text = $"New Tab {tabNumber} content", + Margin = new System.Windows.Thickness(12) + }; + + // Alternatively, you can create a custom TabItem + // e.TabItem = new TabItem + // { + // Header = $"New Tab {tabNumber}", + // Content = new TextBlock { Text = $"New Tab {tabNumber} content" } + // }; } } diff --git a/src/Wpf.Ui/Controls/TabControl/TabControl.xaml b/src/Wpf.Ui/Controls/TabControl/TabControl.xaml index 45599ecd3..9693ef9b7 100644 --- a/src/Wpf.Ui/Controls/TabControl/TabControl.xaml +++ b/src/Wpf.Ui/Controls/TabControl/TabControl.xaml @@ -37,10 +37,12 @@