mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
## Summary of the Pull Request - Changes the Dock height to 38px (from 32) to avoid item and app clipping. - Localization - Removing dead code - If the tooltip string is null or empty, the tooltip will not be shown - Adding hyperlinks on the General and Dock pages in Settings (to be updated to the corresponding docs via aka.ms) - The droptarget for an empty listview is now wider, and has a highlight-color to communicate an item can be dropped: <img width="371" height="142" alt="image" src="https://github.com/user-attachments/assets/6863ca5a-cdd4-450b-ab57-d03d83170cf8" /> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #xxx <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
298 lines
9.8 KiB
C#
298 lines
9.8 KiB
C#
// 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;
|
|
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Controls;
|
|
using Microsoft.UI.Xaml.Input;
|
|
using Microsoft.UI.Xaml.Markup;
|
|
using Microsoft.UI.Xaml.Media;
|
|
|
|
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 =
|
|
DependencyProperty.Register(nameof(ToolTip), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnToolTipPropertyChanged));
|
|
|
|
public string ToolTip
|
|
{
|
|
get => (string)GetValue(ToolTipProperty);
|
|
set => SetValue(ToolTipProperty, value);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
private DockControl? _parentDock;
|
|
private long _dockSideCallbackToken = -1;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
HorizontalAlignment = HorizontalAlignment.Stretch;
|
|
}
|
|
|
|
private void UpdateAllVisibility()
|
|
{
|
|
UpdateTextVisibility();
|
|
UpdateIconVisibility();
|
|
UpdateAlignment();
|
|
}
|
|
|
|
protected override void OnApplyTemplate()
|
|
{
|
|
base.OnApplyTemplate();
|
|
IsEnabledChanged -= OnIsEnabledChanged;
|
|
|
|
PointerEntered -= Control_PointerEntered;
|
|
PointerExited -= Control_PointerExited;
|
|
Loaded -= DockItemControl_Loaded;
|
|
Unloaded -= DockItemControl_Unloaded;
|
|
|
|
PointerEntered += Control_PointerEntered;
|
|
PointerExited += Control_PointerExited;
|
|
Loaded += DockItemControl_Loaded;
|
|
Unloaded += DockItemControl_Unloaded;
|
|
|
|
IsEnabledChanged += OnIsEnabledChanged;
|
|
|
|
// Get template children for visibility updates
|
|
_iconPresenter = GetTemplateChild(IconPresenterName) as FrameworkElement;
|
|
|
|
// Set initial visibility
|
|
UpdateAllVisibility();
|
|
}
|
|
|
|
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),
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|