Enhance TitleBar and CaptionButtons to respect latest Window.CanMaximize and Window.CanMinimize (#815)

* fix: enhance window resizing logic to include maximization check

* fix: update CaptionButtons to control button visibility based on window properties

* fix: update obsolescence messages for minimize and restore button properties

* chore: update comment.
This commit is contained in:
Dong Bin
2025-11-14 13:32:27 +08:00
committed by GitHub
parent d0fc4098dd
commit 0f1a00f388
4 changed files with 187 additions and 50 deletions

View File

@@ -19,18 +19,12 @@ public class CaptionButtons: Avalonia.Controls.Chrome.CaptionButtons
private const string PART_FullScreenButton = "PART_FullScreenButton";
private Button? _closeButton;
private Button? _restoreButton;
private Button? _minimizeButton;
private Button? _fullScreenButton;
private IDisposable? _windowStateSubscription;
private IDisposable? _fullScreenSubscription;
private IDisposable? _minimizeSubscription;
private IDisposable? _restoreSubscription;
private IDisposable? _closeSubscription;
private Button? _minimizeButton;
private Button? _restoreButton;
/// <summary>
/// 切换进入全屏前 窗口的状态
/// Stores the previous window state before entering full-screen mode.
/// </summary>
private WindowState? _oldWindowState;
@@ -45,10 +39,8 @@ public class CaptionButtons: Avalonia.Controls.Chrome.CaptionButtons
Button.ClickEvent.AddHandler((_, _) => OnMinimize(), _minimizeButton);
Button.ClickEvent.AddHandler((_, _) => OnToggleFullScreen(), _fullScreenButton);
if (HostWindow is not null && !HostWindow.CanResize)
{
if (HostWindow is not null && (!HostWindow.CanResize || !HostWindow.CanMaximize))
_restoreButton.IsEnabled = false;
}
UpdateVisibility();
}
@@ -57,58 +49,59 @@ public class CaptionButtons: Avalonia.Controls.Chrome.CaptionButtons
if (HostWindow != null)
{
if (HostWindow.WindowState != WindowState.FullScreen)
{
HostWindow.WindowState = WindowState.FullScreen;
}
else
{
HostWindow.WindowState = _oldWindowState ?? WindowState.Normal;
}
}
}
public override void Attach(Window? hostWindow)
{
if (hostWindow is null) return;
base.Attach(hostWindow);
_windowStateSubscription = HostWindow?.GetPropertyChangedObservable(Window.WindowStateProperty).Subscribe(OnHostWindowStateChanged);
Action<bool> a = _ => UpdateVisibility();
_fullScreenSubscription = HostWindow?.GetObservable(UrsaWindow.IsFullScreenButtonVisibleProperty).Subscribe(a);
_minimizeSubscription = HostWindow?.GetObservable(UrsaWindow.IsMinimizeButtonVisibleProperty).Subscribe(a);
_restoreSubscription = HostWindow?.GetObservable(UrsaWindow.IsRestoreButtonVisibleProperty).Subscribe(a);
_closeSubscription = HostWindow?.GetObservable(UrsaWindow.IsCloseButtonVisibleProperty).Subscribe(a);
if (HostWindow is not null) HostWindow.PropertyChanged += OnWindowPropertyChanged;
}
private void OnHostWindowStateChanged(AvaloniaPropertyChangedEventArgs e)
private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == Window.WindowStateProperty)
{
UpdateVisibility();
if (e.GetNewValue<WindowState>() == WindowState.FullScreen)
{
_oldWindowState = e.GetOldValue<WindowState>();
if (e.GetNewValue<WindowState>() == WindowState.FullScreen) _oldWindowState = e.GetOldValue<WindowState>();
}
if (e.Property == UrsaWindow.IsFullScreenButtonVisibleProperty
|| e.Property == UrsaWindow.IsMinimizeButtonVisibleProperty
|| e.Property == UrsaWindow.IsRestoreButtonVisibleProperty
|| e.Property == UrsaWindow.IsCloseButtonVisibleProperty
|| e.Property == Window.CanMaximizeProperty
|| e.Property == Window.CanMinimizeProperty)
UpdateVisibility();
}
private void UpdateVisibility()
{
if (HostWindow is not UrsaWindow u)
if (HostWindow is UrsaWindow u)
{
return;
}
IsVisibleProperty.SetValue(u.IsCloseButtonVisible, _closeButton);
IsVisibleProperty.SetValue(u.WindowState != WindowState.FullScreen && u.IsRestoreButtonVisible,
IsVisibleProperty.SetValue(
u.CanMaximize && u.WindowState != WindowState.FullScreen && u.IsRestoreButtonVisible,
_restoreButton);
IsVisibleProperty.SetValue(u.WindowState != WindowState.FullScreen && u.IsMinimizeButtonVisible,
IsVisibleProperty.SetValue(
u.CanMinimize && u.WindowState != WindowState.FullScreen && u.IsMinimizeButtonVisible,
_minimizeButton);
IsVisibleProperty.SetValue(u.IsFullScreenButtonVisible, _fullScreenButton);
}
else if (HostWindow is { } s)
{
IsVisibleProperty.SetValue(s.CanMaximize && s.WindowState != WindowState.FullScreen, _restoreButton);
IsVisibleProperty.SetValue(s.CanMinimize && s.WindowState != WindowState.FullScreen, _minimizeButton);
}
}
public override void Detach()
{
if (HostWindow is not null) HostWindow.PropertyChanged -= OnWindowPropertyChanged;
base.Detach();
_windowStateSubscription?.Dispose();
_fullScreenSubscription?.Dispose();
_minimizeSubscription?.Dispose();
_restoreSubscription?.Dispose();
_closeSubscription?.Dispose();
}
}

View File

@@ -29,8 +29,8 @@ public class WindowThumb: Control
private void OnTapped(object? sender, TappedEventArgs e)
{
if (this.VisualRoot is not Window window) return;
if (!window.CanResize) return;
if (VisualRoot is not Window window) return;
if (!window.CanResize || !window.CanMaximize) return;
if ( window.WindowState == WindowState.FullScreen) return;
window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}

View File

@@ -24,6 +24,7 @@ public class UrsaWindow : Window
/// <summary>
/// Defines the visibility of the minimize button.
/// </summary>
[Obsolete("Will be removed in Ursa 2.0. Use Window.CanMinimize property instead.")]
public static readonly StyledProperty<bool> IsMinimizeButtonVisibleProperty =
AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(IsMinimizeButtonVisible), true);
@@ -31,6 +32,7 @@ public class UrsaWindow : Window
/// <summary>
/// Defines the visibility of the restore button.
/// </summary>
[Obsolete("Will be removed in Ursa 2.0. Use Window.CanMaximize property instead.")]
public static readonly StyledProperty<bool> IsRestoreButtonVisibleProperty =
AvaloniaProperty.Register<UrsaWindow, bool>(
nameof(IsRestoreButtonVisible), true);
@@ -101,6 +103,7 @@ public class UrsaWindow : Window
/// <summary>
/// Gets or sets a value indicating whether the minimize button is visible.
/// </summary>
[Obsolete("Will be removed in Ursa 2.0. Use Window.CanMinimize property instead.")]
public bool IsMinimizeButtonVisible
{
get => GetValue(IsMinimizeButtonVisibleProperty);
@@ -110,6 +113,7 @@ public class UrsaWindow : Window
/// <summary>
/// Gets or sets a value indicating whether the restore button is visible.
/// </summary>
[Obsolete("Will be removed in Ursa 2.0. Use Window.CanMaximize property instead.")]
public bool IsRestoreButtonVisible
{
get => GetValue(IsRestoreButtonVisibleProperty);

View File

@@ -0,0 +1,140 @@
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.VisualTree;
using Ursa.Controls;
namespace HeadlessTest.Ursa.Controls.CaptionButtonsTests;
public class Test
{
[AvaloniaTheory]
[InlineData(true, true)]
[InlineData(false, false)]
public void UrsaWindow_IsRestoreButtonVisible_Should_Control_RestoreButton_Visibility(bool canMaximize, bool expectedVisibility)
{
var window = new UrsaWindow();
var caption = new CaptionButtons();
window.Content = caption;
caption.Attach(window);
window.Show();
window.IsRestoreButtonVisible = canMaximize;
var restoreButton = caption.GetVisualDescendants().OfType<Button>().FirstOrDefault(b => b.Name == "PART_RestoreButton");
Assert.NotNull(restoreButton);
Assert.Equal(expectedVisibility, restoreButton.IsVisible);
window.IsRestoreButtonVisible = !canMaximize;
Assert.Equal(!expectedVisibility, restoreButton.IsVisible);
}
[AvaloniaTheory]
[InlineData(true, true)]
[InlineData(false, false)]
public void UrsaWindow_IsMinimizeButtonVisible_Should_Control_MinimizeButton_Visibility(bool canMinimize, bool expectedVisibility)
{
var window = new UrsaWindow();
var caption = new CaptionButtons();
window.Content = caption;
caption.Attach(window);
window.Show();
window.IsMinimizeButtonVisible = canMinimize;
var minimizeButton = caption.GetVisualDescendants().OfType<Button>().FirstOrDefault(b => b.Name == "PART_MinimizeButton");
Assert.NotNull(minimizeButton);
Assert.Equal(expectedVisibility, minimizeButton.IsVisible);
window.IsMinimizeButtonVisible = !canMinimize;
Assert.Equal(!expectedVisibility, minimizeButton.IsVisible);
}
[AvaloniaTheory]
[InlineData(true, true)]
[InlineData(false, false)]
public void UrsaWindow_IsCloseButtonVisible_Should_Control_CloseButton_Visibility(bool isVisible,
bool expectedVisibility)
{
var window = new UrsaWindow();
var caption = new CaptionButtons();
window.Content = caption;
caption.Attach(window);
window.Show();
window.IsCloseButtonVisible = isVisible;
var closeButton = caption.GetVisualDescendants().OfType<Button>()
.FirstOrDefault(b => b.Name == "PART_CloseButton");
Assert.NotNull(closeButton);
Assert.Equal(expectedVisibility, closeButton.IsVisible);
window.IsCloseButtonVisible = !isVisible;
Assert.Equal(!expectedVisibility, closeButton.IsVisible);
}
[AvaloniaTheory]
[InlineData(true, true)]
[InlineData(false, false)]
public void UrsaWindow_IsFullScreenButtonVisible_Should_Control_FullScreenButton_Visibility
(bool isVisible,
bool expectedVisibility)
{
var window = new UrsaWindow();
var caption = new CaptionButtons();
window.Content = caption;
caption.Attach(window);
window.Show();
window.IsFullScreenButtonVisible = isVisible;
var fullScreenButton = caption.GetVisualDescendants().OfType<Button>()
.FirstOrDefault(b => b.Name == "PART_FullScreenButton");
Assert.NotNull(fullScreenButton);
Assert.Equal(expectedVisibility, fullScreenButton.IsVisible);
window.IsFullScreenButtonVisible = !isVisible;
Assert.Equal(!expectedVisibility, fullScreenButton.IsVisible);
}
[AvaloniaTheory]
[InlineData(WindowState.Normal)]
[InlineData(WindowState.Maximized)]
public void CaptionButtons_ToggleFullScreen_Should_Set_WindowState(WindowState initialState)
{
var window = new UrsaWindow();
var caption = new CaptionButtons();
window.Content = caption;
caption.Attach(window);
window.Show();
window.WindowState = initialState;
var fullScreenButton = caption.GetVisualDescendants().OfType<Button>()
.FirstOrDefault(b => b.Name == "PART_FullScreenButton");
Assert.NotNull(fullScreenButton);
fullScreenButton.RaiseEvent(new Avalonia.Interactivity.RoutedEventArgs(Button.ClickEvent));
Assert.Equal(WindowState.FullScreen, window.WindowState);
fullScreenButton.RaiseEvent(new Avalonia.Interactivity.RoutedEventArgs(Button.ClickEvent));
Assert.Equal(initialState, window.WindowState);
}
[AvaloniaTheory]
[InlineData(true, true)]
[InlineData(false, false)]
public void Window_CanMaximize_Should_Update_RestoreButton_Visibility(bool canMaximize, bool expectedVisibility)
{
var window = new UrsaWindow();
var caption = new CaptionButtons();
window.Content = caption;
caption.Attach(window);
window.Show();
window.CanMaximize = canMaximize;
var restoreButton = caption.GetVisualDescendants().OfType<Button>()
.FirstOrDefault(b => b.Name == "PART_RestoreButton");
Assert.NotNull(restoreButton);
Assert.Equal(expectedVisibility, restoreButton.IsVisible);
}
[AvaloniaTheory]
[InlineData(true, true)]
[InlineData(false, false)]
public void Window_CanMinimize_Should_Update_MinimizeButton_Visibility(bool canMinimize , bool expectedVisibility)
{
var window = new UrsaWindow();
var caption = new CaptionButtons();
window.Content = caption;
caption.Attach(window);
window.Show();
window.CanMinimize = canMinimize;
var minimizeButton = caption.GetVisualDescendants().OfType<Button>()
.FirstOrDefault(b => b.Name == "PART_MinimizeButton");
Assert.NotNull(minimizeButton);
Assert.Equal(expectedVisibility, minimizeButton.IsVisible);
}
}