2026-02-27 07:24:23 -06:00
|
|
|
// Copyright (c) Microsoft Corporation
|
|
|
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
|
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
|
|
|
|
|
|
using Microsoft.CmdPal.UI.Controls;
|
|
|
|
|
using Microsoft.CmdPal.UI.ViewModels;
|
|
|
|
|
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
2026-03-01 06:03:40 -06:00
|
|
|
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
2026-02-27 07:24:23 -06:00
|
|
|
using Microsoft.UI.Xaml;
|
|
|
|
|
using Microsoft.UI.Xaml.Controls;
|
|
|
|
|
using Microsoft.UI.Xaml.Input;
|
|
|
|
|
using Microsoft.UI.Xaml.Markup;
|
2026-03-01 06:03:40 -06:00
|
|
|
using Microsoft.UI.Xaml.Media;
|
2026-02-27 07:24:23 -06:00
|
|
|
|
|
|
|
|
namespace Microsoft.CmdPal.UI.Dock;
|
|
|
|
|
|
|
|
|
|
[ContentProperty(Name = nameof(Icon))]
|
|
|
|
|
public sealed partial class DockItemControl : Control
|
|
|
|
|
{
|
|
|
|
|
public DockItemControl()
|
|
|
|
|
{
|
|
|
|
|
DefaultStyleKey = typeof(DockItemControl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty ToolTipProperty =
|
2026-03-06 22:27:23 +01:00
|
|
|
DependencyProperty.Register(nameof(ToolTip), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnToolTipPropertyChanged));
|
2026-02-27 07:24:23 -06:00
|
|
|
|
|
|
|
|
public string ToolTip
|
|
|
|
|
{
|
|
|
|
|
get => (string)GetValue(ToolTipProperty);
|
|
|
|
|
set => SetValue(ToolTipProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 22:27:23 +01:00
|
|
|
private static void OnToolTipPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (d is DockItemControl control)
|
|
|
|
|
{
|
|
|
|
|
// Collapse the tooltip when the string is null or empty so an
|
|
|
|
|
// empty tooltip bubble doesn't appear on hover.
|
|
|
|
|
var text = e.NewValue as string;
|
|
|
|
|
ToolTipService.SetToolTip(control, string.IsNullOrEmpty(text) ? null : text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
public static readonly DependencyProperty TitleProperty =
|
|
|
|
|
DependencyProperty.Register(nameof(Title), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
|
|
|
|
|
|
|
|
|
public string Title
|
|
|
|
|
{
|
|
|
|
|
get => (string)GetValue(TitleProperty);
|
|
|
|
|
set => SetValue(TitleProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty SubtitleProperty =
|
|
|
|
|
DependencyProperty.Register(nameof(Subtitle), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
|
|
|
|
|
|
|
|
|
public string Subtitle
|
|
|
|
|
{
|
|
|
|
|
get => (string)GetValue(SubtitleProperty);
|
|
|
|
|
set => SetValue(SubtitleProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty IconProperty =
|
|
|
|
|
DependencyProperty.Register(nameof(Icon), typeof(object), typeof(DockItemControl), new PropertyMetadata(null, OnIconPropertyChanged));
|
|
|
|
|
|
|
|
|
|
public object Icon
|
|
|
|
|
{
|
|
|
|
|
get => GetValue(IconProperty);
|
|
|
|
|
set => SetValue(IconProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 06:03:40 -06:00
|
|
|
public static readonly DependencyProperty InnerMarginProperty =
|
|
|
|
|
DependencyProperty.Register(nameof(InnerMargin), typeof(Thickness), typeof(DockItemControl), new PropertyMetadata(new Thickness(0)));
|
|
|
|
|
|
|
|
|
|
public Thickness InnerMargin
|
|
|
|
|
{
|
|
|
|
|
get => (Thickness)GetValue(InnerMarginProperty);
|
|
|
|
|
set => SetValue(InnerMarginProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
public static readonly DependencyProperty TextVisibilityProperty =
|
|
|
|
|
DependencyProperty.Register(nameof(TextVisibility), typeof(Visibility), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
|
|
|
|
|
|
|
|
|
public Visibility TextVisibility
|
|
|
|
|
{
|
|
|
|
|
get => (Visibility)GetValue(TextVisibilityProperty);
|
|
|
|
|
set => SetValue(TextVisibilityProperty, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private const string IconPresenterName = "IconPresenter";
|
|
|
|
|
|
|
|
|
|
private FrameworkElement? _iconPresenter;
|
2026-03-01 06:03:40 -06:00
|
|
|
private DockControl? _parentDock;
|
|
|
|
|
private long _dockSideCallbackToken = -1;
|
2026-02-27 07:24:23 -06:00
|
|
|
|
|
|
|
|
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (d is DockItemControl control)
|
|
|
|
|
{
|
|
|
|
|
control.UpdateTextVisibility();
|
|
|
|
|
control.UpdateAlignment();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void OnIconPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (d is DockItemControl control)
|
|
|
|
|
{
|
|
|
|
|
control.UpdateIconVisibility();
|
|
|
|
|
control.UpdateAlignment();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal bool HasTitle => !string.IsNullOrEmpty(Title);
|
|
|
|
|
|
|
|
|
|
internal bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
|
|
|
|
|
|
|
|
|
internal bool HasText => HasTitle || HasSubtitle;
|
|
|
|
|
|
|
|
|
|
private void UpdateTextVisibility()
|
|
|
|
|
{
|
|
|
|
|
UpdateTextVisibilityState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateTextVisibilityState()
|
|
|
|
|
{
|
|
|
|
|
// Determine which visual state to use based on title/subtitle presence
|
|
|
|
|
var stateName = (HasTitle, HasSubtitle) switch
|
|
|
|
|
{
|
|
|
|
|
(true, true) => "TextVisible",
|
|
|
|
|
(true, false) => "TitleOnly",
|
|
|
|
|
(false, true) => "SubtitleOnly",
|
|
|
|
|
(false, false) => "TextHidden",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
VisualStateManager.GoToState(this, stateName, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateIconVisibility()
|
|
|
|
|
{
|
|
|
|
|
if (Icon is IconBox icon)
|
|
|
|
|
{
|
|
|
|
|
var dt = icon.DataContext;
|
|
|
|
|
var src = icon.Source;
|
|
|
|
|
|
|
|
|
|
if (_iconPresenter is not null)
|
|
|
|
|
{
|
|
|
|
|
// n.b. this might be wrong - I think we always have an Icon (an IconBox),
|
|
|
|
|
// we need to check if the box has an icon
|
|
|
|
|
_iconPresenter.Visibility = Icon is null ? Visibility.Collapsed : Visibility.Visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UpdateIconVisibilityState();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateIconVisibilityState()
|
|
|
|
|
{
|
|
|
|
|
var hasIcon = Icon is not null;
|
|
|
|
|
VisualStateManager.GoToState(this, hasIcon ? "IconVisible" : "IconHidden", true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateAlignment()
|
|
|
|
|
{
|
|
|
|
|
// If this item has both an icon and a label, left align so that the
|
|
|
|
|
// icons don't wobble if the text changes.
|
|
|
|
|
//
|
|
|
|
|
// Otherwise, center align.
|
|
|
|
|
var requestedTheme = ActualTheme;
|
|
|
|
|
var isLight = requestedTheme == ElementTheme.Light;
|
|
|
|
|
var showText = HasText;
|
|
|
|
|
if (Icon is IconBox icoBox &&
|
|
|
|
|
icoBox.DataContext is DockItemViewModel item &&
|
|
|
|
|
item.Icon is IconInfoViewModel icon)
|
|
|
|
|
{
|
|
|
|
|
var showIcon = icon is not null && icon.HasIcon(isLight);
|
|
|
|
|
if (showText && showIcon)
|
|
|
|
|
{
|
|
|
|
|
HorizontalAlignment = HorizontalAlignment.Left;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 20:06:42 +01:00
|
|
|
HorizontalAlignment = HorizontalAlignment.Stretch;
|
2026-02-27 07:24:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateAllVisibility()
|
|
|
|
|
{
|
|
|
|
|
UpdateTextVisibility();
|
|
|
|
|
UpdateIconVisibility();
|
|
|
|
|
UpdateAlignment();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void OnApplyTemplate()
|
|
|
|
|
{
|
|
|
|
|
base.OnApplyTemplate();
|
|
|
|
|
IsEnabledChanged -= OnIsEnabledChanged;
|
|
|
|
|
|
|
|
|
|
PointerEntered -= Control_PointerEntered;
|
|
|
|
|
PointerExited -= Control_PointerExited;
|
2026-03-01 06:03:40 -06:00
|
|
|
Loaded -= DockItemControl_Loaded;
|
|
|
|
|
Unloaded -= DockItemControl_Unloaded;
|
2026-02-27 07:24:23 -06:00
|
|
|
|
|
|
|
|
PointerEntered += Control_PointerEntered;
|
|
|
|
|
PointerExited += Control_PointerExited;
|
2026-03-01 06:03:40 -06:00
|
|
|
Loaded += DockItemControl_Loaded;
|
|
|
|
|
Unloaded += DockItemControl_Unloaded;
|
2026-02-27 07:24:23 -06:00
|
|
|
|
|
|
|
|
IsEnabledChanged += OnIsEnabledChanged;
|
|
|
|
|
|
|
|
|
|
// Get template children for visibility updates
|
|
|
|
|
_iconPresenter = GetTemplateChild(IconPresenterName) as FrameworkElement;
|
|
|
|
|
|
|
|
|
|
// Set initial visibility
|
|
|
|
|
UpdateAllVisibility();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 06:03:40 -06:00
|
|
|
private void DockItemControl_Loaded(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
// Walk the visual tree to find our parent DockControl and watch its DockSide.
|
|
|
|
|
// This lets us extend the hit-test area toward the screen edge.
|
|
|
|
|
DependencyObject? parent = VisualTreeHelper.GetParent(this);
|
|
|
|
|
while (parent is not null and not DockControl)
|
|
|
|
|
{
|
|
|
|
|
parent = VisualTreeHelper.GetParent(parent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parent is DockControl dock)
|
|
|
|
|
{
|
|
|
|
|
_parentDock = dock;
|
|
|
|
|
UpdateInnerMarginForDockSide(dock.DockSide);
|
|
|
|
|
_dockSideCallbackToken = dock.RegisterPropertyChangedCallback(
|
|
|
|
|
DockControl.DockSideProperty,
|
|
|
|
|
OnParentDockSideChanged);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DockItemControl_Unloaded(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (_parentDock is not null && _dockSideCallbackToken >= 0)
|
|
|
|
|
{
|
|
|
|
|
_parentDock.UnregisterPropertyChangedCallback(
|
|
|
|
|
DockControl.DockSideProperty,
|
|
|
|
|
_dockSideCallbackToken);
|
|
|
|
|
_dockSideCallbackToken = -1;
|
|
|
|
|
_parentDock = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnParentDockSideChanged(DependencyObject sender, DependencyProperty dp)
|
|
|
|
|
{
|
|
|
|
|
if (sender is DockControl dock)
|
|
|
|
|
{
|
|
|
|
|
UpdateInnerMarginForDockSide(dock.DockSide);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateInnerMarginForDockSide(DockSide side)
|
|
|
|
|
{
|
|
|
|
|
// Push the visual (PART_RootGrid) inward on the screen-edge side so
|
|
|
|
|
// the transparent hit-test area extends all the way to the edge.
|
|
|
|
|
// The values here compensate for the margin/padding removed from the
|
|
|
|
|
// DockControl's ContentGrid on the screen-edge side.
|
|
|
|
|
InnerMargin = side switch
|
|
|
|
|
{
|
|
|
|
|
DockSide.Top => new Thickness(0, 4, 0, 0),
|
|
|
|
|
DockSide.Bottom => new Thickness(0, 0, 0, 4),
|
|
|
|
|
DockSide.Left => new Thickness(8, 0, 0, 0),
|
|
|
|
|
DockSide.Right => new Thickness(0, 0, 8, 0),
|
|
|
|
|
_ => new Thickness(0),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 07:24:23 -06:00
|
|
|
private void Control_PointerEntered(object sender, PointerRoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
VisualStateManager.GoToState(this, "PointerOver", true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Control_PointerExited(object sender, PointerRoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
VisualStateManager.GoToState(this, "Normal", true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void OnPointerPressed(PointerRoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (IsEnabled)
|
|
|
|
|
{
|
|
|
|
|
base.OnPointerPressed(e);
|
|
|
|
|
VisualStateManager.GoToState(this, "Pressed", true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
|
|
|
|
|
}
|
|
|
|
|
}
|