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); } /// Identifies the dependency property. 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; } } /// /// Whether or not the "popup" menu for this control is currently open /// public bool IsExpanded { get => (bool)this.GetValue(IsExpandedProperty); set => this.SetValue(IsExpandedProperty, (bool)value); } /// Identifies the dependency property. public static readonly DependencyProperty ExtraTagProperty = DependencyProperty.Register(nameof(ExtraTag), typeof(object), typeof(DropDownButton)); /// /// Gets or sets an extra tag. /// public object ExtraTag { get => this.GetValue(ExtraTagProperty); set => this.SetValue(ExtraTagProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(DropDownButton), new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure)); /// /// Gets or sets the orientation of children stacking. /// public Orientation Orientation { get => (Orientation)this.GetValue(OrientationProperty); set => this.SetValue(OrientationProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(object), typeof(DropDownButton)); /// /// Gets or sets the content for the icon part. /// [Bindable(true)] public object Icon { get => this.GetValue(IconProperty); set => this.SetValue(IconProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty IconTemplateProperty = DependencyProperty.Register(nameof(IconTemplate), typeof(DataTemplate), typeof(DropDownButton)); /// /// Gets or sets the DataTemplate for the icon part. /// [Bindable(true)] public DataTemplate IconTemplate { get => (DataTemplate)this.GetValue(IconTemplateProperty); set => this.SetValue(IconTemplateProperty, value); } /// Identifies the dependency property. 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); } /// /// Gets or sets the command to invoke when the content button is pressed. /// public ICommand Command { get => (ICommand)this.GetValue(CommandProperty); set => this.SetValue(CommandProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty CommandTargetProperty = DependencyProperty.Register(nameof(CommandTarget), typeof(IInputElement), typeof(DropDownButton), new PropertyMetadata(null)); /// /// Gets or sets the element on which to raise the specified command. /// public IInputElement CommandTarget { get => (IInputElement)this.GetValue(CommandTargetProperty); set => this.SetValue(CommandTargetProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(nameof(CommandParameter), typeof(object), typeof(DropDownButton), new PropertyMetadata(null)); /// /// Gets or sets the parameter to pass to the command property. /// public object CommandParameter { get => (object)this.GetValue(CommandParameterProperty); set => this.SetValue(CommandParameterProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(DropDownButton)); /// /// Gets or sets the content of this control. /// public object Content { get => (object)this.GetValue(ContentProperty); set => this.SetValue(ContentProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty ContentTemplateProperty = DependencyProperty.Register(nameof(ContentTemplate), typeof(DataTemplate), typeof(DropDownButton), new FrameworkPropertyMetadata(null)); /// /// Gets or sets the data template used to display the content of the DropDownButton. /// [Bindable(true)] public DataTemplate ContentTemplate { get => (DataTemplate)this.GetValue(ContentTemplateProperty); set => this.SetValue(ContentTemplateProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty ContentTemplateSelectorProperty = DependencyProperty.Register(nameof(ContentTemplateSelector), typeof(DataTemplateSelector), typeof(DropDownButton), new FrameworkPropertyMetadata(null)); /// /// Gets or sets a template selector that enables an application writer to provide custom template-selection logic. /// /// /// This property is ignored if is set. /// [Bindable(true)] public DataTemplateSelector ContentTemplateSelector { get => (DataTemplateSelector)this.GetValue(ContentTemplateSelectorProperty); set => this.SetValue(ContentTemplateSelectorProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty ContentStringFormatProperty = DependencyProperty.Register(nameof(ContentStringFormat), typeof(string), typeof(DropDownButton), new FrameworkPropertyMetadata(null)); /// /// Gets or sets a composite string that specifies how to format the content property if it is displayed as a string. /// /// /// This property is ignored if is set. /// [Bindable(true)] public string ContentStringFormat { get => (string)this.GetValue(ContentStringFormatProperty); set => this.SetValue(ContentStringFormatProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty ButtonStyleProperty = DependencyProperty.Register(nameof(ButtonStyle), typeof(Style), typeof(DropDownButton), new FrameworkPropertyMetadata(default(Style), FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure)); /// /// Gets or sets the button content style. /// public Style ButtonStyle { get => (Style)this.GetValue(ButtonStyleProperty); set => this.SetValue(ButtonStyleProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty MenuStyleProperty = DependencyProperty.Register(nameof(MenuStyle), typeof(Style), typeof(DropDownButton), new FrameworkPropertyMetadata(default(Style), FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure)); /// /// Gets or sets the "popup" menu style. /// public Style MenuStyle { get => (Style)this.GetValue(MenuStyleProperty); set => this.SetValue(MenuStyleProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty ArrowBrushProperty = DependencyProperty.Register(nameof(ArrowBrush), typeof(Brush), typeof(DropDownButton), new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender)); /// /// Gets or sets the foreground brush for the button arrow icon. /// public Brush ArrowBrush { get => (Brush)this.GetValue(ArrowBrushProperty); set => this.SetValue(ArrowBrushProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty ArrowMouseOverBrushProperty = DependencyProperty.Register(nameof(ArrowMouseOverBrush), typeof(Brush), typeof(DropDownButton), new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender)); /// /// Gets or sets the foreground brush of the button arrow icon if the mouse is over the drop down button. /// public Brush ArrowMouseOverBrush { get => (Brush)this.GetValue(ArrowMouseOverBrushProperty); set => this.SetValue(ArrowMouseOverBrushProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty ArrowPressedBrushProperty = DependencyProperty.Register(nameof(ArrowPressedBrush), typeof(Brush), typeof(DropDownButton), new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender)); /// /// Gets or sets the foreground brush of the button arrow icon if the arrow button is pressed. /// public Brush ArrowPressedBrush { get => (Brush)this.GetValue(ArrowPressedBrushProperty); set => this.SetValue(ArrowPressedBrushProperty, value); } /// Identifies the dependency property. public static readonly DependencyProperty ArrowVisibilityProperty = DependencyProperty.Register(nameof(ArrowVisibility), typeof(Visibility), typeof(DropDownButton), new FrameworkPropertyMetadata(Visibility.Visible, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure)); /// /// Gets or sets the visibility of the button arrow icon. /// 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); } /// 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()) { this.contextMenu?.GroupStyle.Remove(groupStyle); } } if (e.NewItems != null) { foreach (var groupStyle in e.NewItems.OfType()) { this.contextMenu?.GroupStyle.Add(groupStyle); } } } /// 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); } } } /// Invoked when the property changes. /// Information about the change. 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); } } } } }