Files
aistudio-wpf-diagram/AIStudio.Wpf.Mind/Controls/DropDownButton.xaml.cs
2023-03-05 21:30:53 +08:00

686 lines
27 KiB
C#

using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Security;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
namespace AIStudio.Wpf.Mind.Controls
{
[ContentProperty(nameof(ItemsSource))]
[TemplatePart(Name = "PART_Button", Type = typeof(Button))]
[TemplatePart(Name = "PART_ButtonContent", Type = typeof(ContentControl))]
[TemplatePart(Name = "PART_Menu", Type = typeof(ContextMenu))]
[StyleTypedProperty(Property = nameof(ButtonStyle), StyleTargetType = typeof(Button))]
[StyleTypedProperty(Property = nameof(MenuStyle), StyleTargetType = typeof(ContextMenu))]
public class DropDownButton : ItemsControl, ICommandSource
{
public static readonly RoutedEvent ClickEvent
= EventManager.RegisterRoutedEvent(nameof(Click),
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(DropDownButton));
public event RoutedEventHandler Click
{
add => this.AddHandler(ClickEvent, value);
remove => this.RemoveHandler(ClickEvent, value);
}
/// <summary>Identifies the <see cref="IsExpanded"/> dependency property.</summary>
public static readonly DependencyProperty IsExpandedProperty
= DependencyProperty.Register(nameof(IsExpanded),
typeof(bool),
typeof(DropDownButton),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnIsExpandedPropertyChangedCallback));
private static void OnIsExpandedPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
if (dependencyObject is DropDownButton dropDownButton
&& !(dropDownButton.contextMenu == null))
{
dropDownButton.SetContextMenuPlacementTarget(dropDownButton.contextMenu);
}
}
protected virtual void SetContextMenuPlacementTarget(ContextMenu contextMenu)
{
if (this.button != null)
{
contextMenu.PlacementTarget = this.button;
}
}
/// <summary>
/// Whether or not the "popup" menu for this control is currently open
/// </summary>
public bool IsExpanded
{
get => (bool)this.GetValue(IsExpandedProperty);
set => this.SetValue(IsExpandedProperty, (bool)value);
}
/// <summary>Identifies the <see cref="ExtraTag"/> dependency property.</summary>
public static readonly DependencyProperty ExtraTagProperty
= DependencyProperty.Register(nameof(ExtraTag),
typeof(object),
typeof(DropDownButton));
/// <summary>
/// Gets or sets an extra tag.
/// </summary>
public object ExtraTag
{
get => this.GetValue(ExtraTagProperty);
set => this.SetValue(ExtraTagProperty, value);
}
/// <summary>Identifies the <see cref="Orientation"/> dependency property.</summary>
public static readonly DependencyProperty OrientationProperty
= DependencyProperty.Register(nameof(Orientation),
typeof(Orientation),
typeof(DropDownButton),
new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// Gets or sets the orientation of children stacking.
/// </summary>
public Orientation Orientation
{
get => (Orientation)this.GetValue(OrientationProperty);
set => this.SetValue(OrientationProperty, value);
}
/// <summary>Identifies the <see cref="Icon"/> dependency property.</summary>
public static readonly DependencyProperty IconProperty
= DependencyProperty.Register(nameof(Icon),
typeof(object),
typeof(DropDownButton));
/// <summary>
/// Gets or sets the content for the icon part.
/// </summary>
[Bindable(true)]
public object Icon
{
get => this.GetValue(IconProperty);
set => this.SetValue(IconProperty, value);
}
/// <summary>Identifies the <see cref="IconTemplate"/> dependency property.</summary>
public static readonly DependencyProperty IconTemplateProperty
= DependencyProperty.Register(nameof(IconTemplate),
typeof(DataTemplate),
typeof(DropDownButton));
/// <summary>
/// Gets or sets the DataTemplate for the icon part.
/// </summary>
[Bindable(true)]
public DataTemplate IconTemplate
{
get => (DataTemplate)this.GetValue(IconTemplateProperty);
set => this.SetValue(IconTemplateProperty, value);
}
/// <summary>Identifies the <see cref="Command"/> dependency property.</summary>
public static readonly DependencyProperty CommandProperty
= DependencyProperty.Register(nameof(Command),
typeof(ICommand),
typeof(DropDownButton),
new PropertyMetadata(null, OnCommandPropertyChangedCallback));
private static void OnCommandPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
(dependencyObject as DropDownButton).OnCommandChanged((ICommand)e.OldValue, (ICommand)e.NewValue);
}
/// <summary>
/// Gets or sets the command to invoke when the content button is pressed.
/// </summary>
public ICommand Command
{
get => (ICommand)this.GetValue(CommandProperty);
set => this.SetValue(CommandProperty, value);
}
/// <summary>Identifies the <see cref="CommandTarget"/> dependency property.</summary>
public static readonly DependencyProperty CommandTargetProperty
= DependencyProperty.Register(nameof(CommandTarget),
typeof(IInputElement),
typeof(DropDownButton),
new PropertyMetadata(null));
/// <summary>
/// Gets or sets the element on which to raise the specified command.
/// </summary>
public IInputElement CommandTarget
{
get => (IInputElement)this.GetValue(CommandTargetProperty);
set => this.SetValue(CommandTargetProperty, value);
}
/// <summary>Identifies the <see cref="CommandParameter"/> dependency property.</summary>
public static readonly DependencyProperty CommandParameterProperty
= DependencyProperty.Register(nameof(CommandParameter),
typeof(object),
typeof(DropDownButton),
new PropertyMetadata(null));
/// <summary>
/// Gets or sets the parameter to pass to the command property.
/// </summary>
public object CommandParameter
{
get => (object)this.GetValue(CommandParameterProperty);
set => this.SetValue(CommandParameterProperty, value);
}
/// <summary>Identifies the <see cref="Content"/> dependency property.</summary>
public static readonly DependencyProperty ContentProperty
= DependencyProperty.Register(nameof(Content),
typeof(object),
typeof(DropDownButton));
/// <summary>
/// Gets or sets the content of this control.
/// </summary>
public object Content
{
get => (object)this.GetValue(ContentProperty);
set => this.SetValue(ContentProperty, value);
}
/// <summary>Identifies the <see cref="ContentTemplate"/> dependency property.</summary>
public static readonly DependencyProperty ContentTemplateProperty
= DependencyProperty.Register(nameof(ContentTemplate),
typeof(DataTemplate),
typeof(DropDownButton),
new FrameworkPropertyMetadata(null));
/// <summary>
/// Gets or sets the data template used to display the content of the DropDownButton.
/// </summary>
[Bindable(true)]
public DataTemplate ContentTemplate
{
get => (DataTemplate)this.GetValue(ContentTemplateProperty);
set => this.SetValue(ContentTemplateProperty, value);
}
/// <summary>Identifies the <see cref="ContentTemplateSelector"/> dependency property.</summary>
public static readonly DependencyProperty ContentTemplateSelectorProperty
= DependencyProperty.Register(nameof(ContentTemplateSelector),
typeof(DataTemplateSelector),
typeof(DropDownButton),
new FrameworkPropertyMetadata(null));
/// <summary>
/// Gets or sets a template selector that enables an application writer to provide custom template-selection logic.
/// </summary>
/// <remarks>
/// This property is ignored if <seealso cref="ContentTemplate"/> is set.
/// </remarks>
[Bindable(true)]
public DataTemplateSelector ContentTemplateSelector
{
get => (DataTemplateSelector)this.GetValue(ContentTemplateSelectorProperty);
set => this.SetValue(ContentTemplateSelectorProperty, value);
}
/// <summary>Identifies the <see cref="ContentStringFormat"/> dependency property.</summary>
public static readonly DependencyProperty ContentStringFormatProperty
= DependencyProperty.Register(nameof(ContentStringFormat),
typeof(string),
typeof(DropDownButton),
new FrameworkPropertyMetadata(null));
/// <summary>
/// Gets or sets a composite string that specifies how to format the content property if it is displayed as a string.
/// </summary>
/// <remarks>
/// This property is ignored if <seealso cref="ContentTemplate"/> is set.
/// </remarks>
[Bindable(true)]
public string ContentStringFormat
{
get => (string)this.GetValue(ContentStringFormatProperty);
set => this.SetValue(ContentStringFormatProperty, value);
}
/// <summary>Identifies the <see cref="ButtonStyle"/> dependency property.</summary>
public static readonly DependencyProperty ButtonStyleProperty
= DependencyProperty.Register(nameof(ButtonStyle),
typeof(Style),
typeof(DropDownButton),
new FrameworkPropertyMetadata(default(Style), FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// Gets or sets the button content style.
/// </summary>
public Style ButtonStyle
{
get => (Style)this.GetValue(ButtonStyleProperty);
set => this.SetValue(ButtonStyleProperty, value);
}
/// <summary>Identifies the <see cref="MenuStyle"/> dependency property.</summary>
public static readonly DependencyProperty MenuStyleProperty
= DependencyProperty.Register(nameof(MenuStyle),
typeof(Style),
typeof(DropDownButton),
new FrameworkPropertyMetadata(default(Style), FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// Gets or sets the "popup" menu style.
/// </summary>
public Style MenuStyle
{
get => (Style)this.GetValue(MenuStyleProperty);
set => this.SetValue(MenuStyleProperty, value);
}
/// <summary>Identifies the <see cref="ArrowBrush"/> dependency property.</summary>
public static readonly DependencyProperty ArrowBrushProperty
= DependencyProperty.Register(nameof(ArrowBrush),
typeof(Brush),
typeof(DropDownButton),
new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender));
/// <summary>
/// Gets or sets the foreground brush for the button arrow icon.
/// </summary>
public Brush ArrowBrush
{
get => (Brush)this.GetValue(ArrowBrushProperty);
set => this.SetValue(ArrowBrushProperty, value);
}
/// <summary>Identifies the <see cref="ArrowMouseOverBrush"/> dependency property.</summary>
public static readonly DependencyProperty ArrowMouseOverBrushProperty
= DependencyProperty.Register(nameof(ArrowMouseOverBrush),
typeof(Brush),
typeof(DropDownButton),
new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender));
/// <summary>
/// Gets or sets the foreground brush of the button arrow icon if the mouse is over the drop down button.
/// </summary>
public Brush ArrowMouseOverBrush
{
get => (Brush)this.GetValue(ArrowMouseOverBrushProperty);
set => this.SetValue(ArrowMouseOverBrushProperty, value);
}
/// <summary>Identifies the <see cref="ArrowPressedBrush"/> dependency property.</summary>
public static readonly DependencyProperty ArrowPressedBrushProperty
= DependencyProperty.Register(nameof(ArrowPressedBrush),
typeof(Brush),
typeof(DropDownButton),
new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender));
/// <summary>
/// Gets or sets the foreground brush of the button arrow icon if the arrow button is pressed.
/// </summary>
public Brush ArrowPressedBrush
{
get => (Brush)this.GetValue(ArrowPressedBrushProperty);
set => this.SetValue(ArrowPressedBrushProperty, value);
}
/// <summary>Identifies the <see cref="ArrowVisibility"/> dependency property.</summary>
public static readonly DependencyProperty ArrowVisibilityProperty
= DependencyProperty.Register(nameof(ArrowVisibility),
typeof(Visibility),
typeof(DropDownButton),
new FrameworkPropertyMetadata(Visibility.Visible, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// Gets or sets the visibility of the button arrow icon.
/// </summary>
public Visibility ArrowVisibility
{
get => (Visibility)this.GetValue(ArrowVisibilityProperty);
set => this.SetValue(ArrowVisibilityProperty, value);
}
static DropDownButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DropDownButton), new FrameworkPropertyMetadata(typeof(DropDownButton)));
}
private void OnCommandChanged(ICommand oldCommand, ICommand newCommand)
{
if (oldCommand != null)
{
this.UnhookCommand(oldCommand);
}
if (newCommand != null)
{
this.HookCommand(newCommand);
}
}
private void UnhookCommand(ICommand command)
{
CanExecuteChangedEventManager.RemoveHandler(command, this.OnCanExecuteChanged);
this.UpdateCanExecute();
}
private void HookCommand(ICommand command)
{
CanExecuteChangedEventManager.AddHandler(command, this.OnCanExecuteChanged);
this.UpdateCanExecute();
}
private void OnCanExecuteChanged(object sender, EventArgs e)
{
this.UpdateCanExecute();
}
private void UpdateCanExecute()
{
this.CanExecute = this.Command == null || CommandHelpers.CanExecuteCommandSource(this);
}
/// <inheritdoc />
protected override bool IsEnabledCore => base.IsEnabledCore && this.CanExecute;
private bool canExecute = true;
private bool CanExecute
{
get => this.canExecute;
set
{
if (value == this.canExecute)
{
return;
}
this.canExecute = value;
this.CoerceValue(IsEnabledProperty);
}
}
private void ButtonClick(object sender, RoutedEventArgs e)
{
CommandHelpers.ExecuteCommandSource(this);
if (this.contextMenu?.HasItems == true)
{
this.SetCurrentValue(IsExpandedProperty, true);
}
e.RoutedEvent = ClickEvent;
this.RaiseEvent(e);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (this.button != null)
{
this.button.Click -= this.ButtonClick;
}
this.button = this.GetTemplateChild("PART_Button") as Button;
if (this.button != null)
{
this.button.Click += this.ButtonClick;
}
this.GroupStyle.CollectionChanged -= this.OnGroupStyleCollectionChanged;
this.contextMenu = this.GetTemplateChild("PART_Menu") as ContextMenu;
if (this.contextMenu != null)
{
foreach (var groupStyle in this.GroupStyle)
{
this.contextMenu.GroupStyle.Add(groupStyle);
}
this.GroupStyle.CollectionChanged += this.OnGroupStyleCollectionChanged;
if (this.Items != null && this.ItemsSource == null)
{
foreach (var newItem in this.Items)
{
this.TryRemoveVisualFromOldTree(newItem);
this.contextMenu.Items.Add(newItem);
}
}
}
}
#if NET5_0_OR_GREATER
private void OnGroupStyleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
#else
private void OnGroupStyleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
#endif
{
if (e.OldItems != null)
{
foreach (var groupStyle in e.OldItems.OfType<GroupStyle>())
{
this.contextMenu?.GroupStyle.Remove(groupStyle);
}
}
if (e.NewItems != null)
{
foreach (var groupStyle in e.NewItems.OfType<GroupStyle>())
{
this.contextMenu?.GroupStyle.Add(groupStyle);
}
}
}
/// <inheritdoc />
protected override void OnMouseRightButtonUp(MouseButtonEventArgs e)
{
base.OnMouseRightButtonUp(e);
e.Handled = true;
}
private void TryRemoveVisualFromOldTree(object item)
{
if (item is Visual visual)
{
var parent = LogicalTreeHelper.GetParent(visual) as FrameworkElement ?? VisualTreeHelper.GetParent(visual) as FrameworkElement;
if (Equals(this, parent))
{
this.RemoveLogicalChild(visual);
this.RemoveVisualChild(visual);
}
}
}
/// <summary>Invoked when the <see cref="P:System.Windows.Controls.ItemsControl.Items" /> property changes.</summary>
/// <param name="e">Information about the change.</param>
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (this.contextMenu == null || this.ItemsSource != null || this.contextMenu.ItemsSource != null)
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewItems != null)
{
foreach (var newItem in e.NewItems)
{
this.TryRemoveVisualFromOldTree(newItem);
this.contextMenu.Items.Add(newItem);
}
}
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldItems != null)
{
foreach (var oldItem in e.OldItems)
{
this.contextMenu.Items.Remove(oldItem);
}
}
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
if (e.OldItems != null)
{
foreach (var oldItem in e.OldItems)
{
this.contextMenu.Items.Remove(oldItem);
}
}
if (e.NewItems != null)
{
foreach (var newItem in e.NewItems)
{
this.TryRemoveVisualFromOldTree(newItem);
this.contextMenu.Items.Add(newItem);
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (this.Items != null)
{
this.contextMenu.Items.Clear();
foreach (var newItem in this.Items)
{
this.TryRemoveVisualFromOldTree(newItem);
this.contextMenu.Items.Add(newItem);
}
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private Button button;
private ContextMenu contextMenu;
}
internal static class CommandHelpers
{
internal static bool CanExecuteCommandSource(ICommandSource commandSource)
{
var command = commandSource.Command;
if (command == null)
{
return false;
}
var commandParameter = commandSource.CommandParameter ?? commandSource;
if (command is RoutedCommand routedCommand)
{
var target = commandSource.CommandTarget ?? commandSource as IInputElement;
return routedCommand.CanExecute(commandParameter, target);
}
return command.CanExecute(commandParameter);
}
[SecurityCritical]
[SecuritySafeCritical]
internal static void ExecuteCommandSource(ICommandSource commandSource)
{
CriticalExecuteCommandSource(commandSource);
}
[SecurityCritical]
internal static void CriticalExecuteCommandSource(ICommandSource commandSource)
{
var command = commandSource.Command;
if (command == null)
{
return;
}
var commandParameter = commandSource.CommandParameter ?? commandSource;
if (command is RoutedCommand routedCommand)
{
var target = commandSource.CommandTarget ?? commandSource as IInputElement;
if (routedCommand.CanExecute(commandParameter, target))
{
routedCommand.Execute(commandParameter, target);
}
}
else
{
if (command.CanExecute(commandParameter))
{
command.Execute(commandParameter);
}
}
}
internal static bool CanExecuteCommandSource(ICommandSource commandSource, ICommand theCommand)
{
var command = theCommand;
if (command == null)
{
return false;
}
var commandParameter = commandSource.CommandParameter ?? commandSource;
if (command is RoutedCommand routedCommand)
{
var target = commandSource.CommandTarget ?? commandSource as IInputElement;
return routedCommand.CanExecute(commandParameter, target);
}
return command.CanExecute(commandParameter);
}
[SecurityCritical]
[SecuritySafeCritical]
internal static void ExecuteCommandSource(ICommandSource commandSource, ICommand theCommand)
{
CriticalExecuteCommandSource(commandSource, theCommand);
}
[SecurityCritical]
internal static void CriticalExecuteCommandSource(ICommandSource commandSource, ICommand theCommand)
{
var command = theCommand;
if (command == null)
{
return;
}
var commandParameter = commandSource.CommandParameter ?? commandSource;
if (command is RoutedCommand routedCommand)
{
var target = commandSource.CommandTarget ?? commandSource as IInputElement;
if (routedCommand.CanExecute(commandParameter, target))
{
routedCommand.Execute(commandParameter, target);
}
}
else
{
if (command.CanExecute(commandParameter))
{
command.Execute(commandParameter);
}
}
}
}
}