Browse Source

Add Window.CanMinimize/CanMaximize (#18117)

* Add CanMinimize and CanMaximize to Window

* Win32 impl for CanMinimize/CanMaximize

* Add CanResize/CanMinimize/CanMaximize samples to control catalog

* X11 impl for CanMinimize/CanMaximize

* macOS impl for CanMinimize/CanMaximize

* Win32: don't allow restore when the window isn't resizable

* Additional documentation for CanMinimize/CanMaximize

* Add CanMinimize/CanMaximize logic to CaptionButtons

* Use START_COM_ARP_CALL

* Added CanMinimize/CanMaximize integration tests

* Fixed CanMaximize tests on macOS
pull/19461/head
Julien Lebosquain 6 months ago
committed by GitHub
parent
commit
1f8701a2fc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      native/Avalonia.Native/src/OSX/WindowImpl.h
  2. 27
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  3. 3
      samples/ControlCatalog/MainWindow.xaml
  4. 49
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  5. 64
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  6. 6
      samples/IntegrationTestApp/Pages/WindowPage.axaml
  7. 5
      samples/IntegrationTestApp/Pages/WindowPage.axaml.cs
  8. 55
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  9. 11
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  10. 53
      src/Avalonia.Controls/Window.cs
  11. 8
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  12. 8
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  13. 25
      src/Avalonia.Native/WindowImpl.cs
  14. 2
      src/Avalonia.Native/avn.idl
  15. 2
      src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml
  16. 38
      src/Avalonia.X11/X11Window.cs
  17. 8
      src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
  18. 2
      src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs
  19. 2
      src/Windows/Avalonia.Win32/PopupImpl.cs
  20. 36
      src/Windows/Avalonia.Win32/WindowImpl.cs
  21. 17
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  22. 43
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

8
native/Avalonia.Native/src/OSX/WindowImpl.h

@ -45,6 +45,10 @@ BEGIN_INTERFACE_MAP()
void DoZoom(); void DoZoom();
virtual HRESULT SetCanResize(bool value) override; virtual HRESULT SetCanResize(bool value) override;
virtual HRESULT SetCanMinimize(bool value) override;
virtual HRESULT SetCanMaximize(bool value) override;
virtual HRESULT SetDecorations(SystemDecorations value) override; virtual HRESULT SetDecorations(SystemDecorations value) override;
@ -82,7 +86,7 @@ BEGIN_INTERFACE_MAP()
bool CanBecomeKeyWindow (); bool CanBecomeKeyWindow ();
bool CanZoom() override { return _isEnabled && _canResize; } bool CanZoom() override { return _isEnabled && _canMaximize; }
protected: protected:
virtual NSWindowStyleMask CalculateStyleMask() override; virtual NSWindowStyleMask CalculateStyleMask() override;
@ -94,6 +98,8 @@ private:
NSString *_lastTitle; NSString *_lastTitle;
bool _isEnabled; bool _isEnabled;
bool _canResize; bool _canResize;
bool _canMinimize;
bool _canMaximize;
bool _fullScreenActive; bool _fullScreenActive;
SystemDecorations _decorations; SystemDecorations _decorations;
AvnWindowState _lastWindowState; AvnWindowState _lastWindowState;

27
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -16,6 +16,8 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events) : TopLevelImpl(events), WindowB
_extendClientHints = AvnDefaultChrome; _extendClientHints = AvnDefaultChrome;
_fullScreenActive = false; _fullScreenActive = false;
_canResize = true; _canResize = true;
_canMinimize = true;
_canMaximize = true;
_decorations = SystemDecorationsFull; _decorations = SystemDecorationsFull;
_transitioningWindowState = false; _transitioningWindowState = false;
_inSetWindowState = false; _inSetWindowState = false;
@ -191,7 +193,8 @@ bool WindowImpl::IsZoomed() {
void WindowImpl::DoZoom() { void WindowImpl::DoZoom() {
if (_decorations == SystemDecorationsNone || if (_decorations == SystemDecorationsNone ||
_decorations == SystemDecorationsBorderOnly || _decorations == SystemDecorationsBorderOnly ||
_canResize == false) { _canResize == false ||
_canMaximize == false) {
[Window setFrame:[Window screen].visibleFrame display:true]; [Window setFrame:[Window screen].visibleFrame display:true];
} else { } else {
[Window performZoom:Window]; [Window performZoom:Window];
@ -208,6 +211,22 @@ HRESULT WindowImpl::SetCanResize(bool value) {
} }
} }
HRESULT WindowImpl::SetCanMinimize(bool value) {
START_COM_ARP_CALL;
_canMinimize = value;
UpdateAppearance();
return S_OK;
}
HRESULT WindowImpl::SetCanMaximize(bool value) {
START_COM_ARP_CALL;
_canMaximize = value;
UpdateAppearance();
return S_OK;
}
HRESULT WindowImpl::SetDecorations(SystemDecorations value) { HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
START_COM_CALL; START_COM_CALL;
@ -583,7 +602,7 @@ NSWindowStyleMask WindowImpl::CalculateStyleMask() {
break; break;
} }
if (!IsOwned()) { if (_canMinimize && !IsOwned()) {
s |= NSWindowStyleMaskMiniaturizable; s |= NSWindowStyleMaskMiniaturizable;
} }
@ -611,9 +630,9 @@ void WindowImpl::UpdateAppearance() {
[closeButton setHidden:!hasTrafficLights]; [closeButton setHidden:!hasTrafficLights];
[closeButton setEnabled:_isEnabled]; [closeButton setEnabled:_isEnabled];
[miniaturizeButton setHidden:!hasTrafficLights]; [miniaturizeButton setHidden:!hasTrafficLights];
[miniaturizeButton setEnabled:_isEnabled]; [miniaturizeButton setEnabled:_isEnabled && _canMinimize];
[zoomButton setHidden:!hasTrafficLights]; [zoomButton setHidden:!hasTrafficLights];
[zoomButton setEnabled:CanZoom()]; [zoomButton setEnabled:CanZoom() || (([Window styleMask] & NSWindowStyleMaskFullScreen) != 0 && _isEnabled)];
} }
extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events) extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events)

3
samples/ControlCatalog/MainWindow.xaml

@ -10,6 +10,9 @@
ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}"
ExtendClientAreaChromeHints="{Binding ChromeHints}" ExtendClientAreaChromeHints="{Binding ChromeHints}"
ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
CanResize="{Binding CanResize}"
CanMinimize="{Binding CanMinimize}"
CanMaximize="{Binding CanMaximize}"
x:Name="MainWindow" x:Name="MainWindow"
Background="Transparent" Background="Transparent"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"

49
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@ -8,18 +8,55 @@
x:DataType="viewModels:MainWindowViewModel" x:DataType="viewModels:MainWindowViewModel"
x:CompileBindings="True"> x:CompileBindings="True">
<StackPanel> <StackPanel>
<StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Desktop=true}">
<TextBlock Classes="h2" Text="Desktop properties" Margin="4" /> <StackPanel
<CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" /> Spacing="10"
<CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" /> Margin="25"
<CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" /> IsEnabled="{OnFormFactor false, Desktop=true}">
<Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
<TextBlock Classes="h2"
Text="Desktop properties"
Margin="4" />
<CheckBox Content="Extend Client Area to Decorations"
IsChecked="{Binding ExtendClientAreaEnabled}" />
<DockPanel IsEnabled="{Binding ExtendClientAreaEnabled}">
<CheckBox Content="Title Bar"
IsChecked="{Binding SystemTitleBarEnabled}"
DockPanel.Dock="Left" />
<Slider Minimum="-1"
Maximum="200"
Value="{Binding TitleBarHeight}"
IsEnabled="{Binding SystemTitleBarEnabled}"
Margin="8,-10" />
</DockPanel>
<CheckBox Content="Prefer System Chrome"
IsChecked="{Binding PreferSystemChromeEnabled}"
IsEnabled="{Binding ExtendClientAreaEnabled}" />
<CheckBox Content="Can Resize"
IsChecked="{Binding CanResize}" />
<CheckBox Content="Can Minimize"
IsChecked="{Binding CanMinimize}" />
<CheckBox Content="Can Maximize"
IsChecked="{Binding CanMaximize}"
IsEnabled="{Binding CanResize}" />
</StackPanel> </StackPanel>
<StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Mobile=true}"> <StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Mobile=true}">
<TextBlock Classes="h2" Text="Mobile properties" Margin="4" /> <TextBlock Classes="h2" Text="Mobile properties" Margin="4" />
<CheckBox Content="Is System Bar Visible" IsChecked="{Binding IsSystemBarVisible}" /> <CheckBox Content="Is System Bar Visible" IsChecked="{Binding IsSystemBarVisible}" />
<CheckBox Content="Display Edge To Edge" IsChecked="{Binding DisplayEdgeToEdge}" /> <CheckBox Content="Display Edge To Edge" IsChecked="{Binding DisplayEdgeToEdge}" />
<TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}" /> <TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

64
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,6 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.Dialogs; using Avalonia.Dialogs;
using Avalonia.Platform; using Avalonia.Platform;
using System; using System;
@ -22,6 +21,9 @@ namespace ControlCatalog.ViewModels
private bool _isSystemBarVisible; private bool _isSystemBarVisible;
private bool _displayEdgeToEdge; private bool _displayEdgeToEdge;
private Thickness _safeAreaPadding; private Thickness _safeAreaPadding;
private bool _canResize;
private bool _canMinimize;
private bool _canMaximize;
public MainWindowViewModel() public MainWindowViewModel()
{ {
@ -49,7 +51,7 @@ namespace ControlCatalog.ViewModels
WindowState.FullScreen, WindowState.FullScreen,
}; };
this.PropertyChanged += (s, e) => PropertyChanged += (s, e) =>
{ {
if (e.PropertyName is nameof(SystemTitleBarEnabled) or nameof(PreferSystemChromeEnabled)) if (e.PropertyName is nameof(SystemTitleBarEnabled) or nameof(PreferSystemChromeEnabled))
{ {
@ -67,70 +69,104 @@ namespace ControlCatalog.ViewModels
} }
}; };
SystemTitleBarEnabled = true; SystemTitleBarEnabled = true;
TitleBarHeight = -1; TitleBarHeight = -1;
CanResize = true;
CanMinimize = true;
CanMaximize = true;
} }
public ExtendClientAreaChromeHints ChromeHints public ExtendClientAreaChromeHints ChromeHints
{ {
get { return _chromeHints; } get { return _chromeHints; }
set { this.RaiseAndSetIfChanged(ref _chromeHints, value); } set { RaiseAndSetIfChanged(ref _chromeHints, value); }
} }
public bool ExtendClientAreaEnabled public bool ExtendClientAreaEnabled
{ {
get { return _extendClientAreaEnabled; } get { return _extendClientAreaEnabled; }
set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } set
{
if (RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value) && !value)
{
SystemTitleBarEnabled = true;
}
}
} }
public bool SystemTitleBarEnabled public bool SystemTitleBarEnabled
{ {
get { return _systemTitleBarEnabled; } get { return _systemTitleBarEnabled; }
set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); } set
{
if (RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value) && !value)
{
TitleBarHeight = -1;
}
}
} }
public bool PreferSystemChromeEnabled public bool PreferSystemChromeEnabled
{ {
get { return _preferSystemChromeEnabled; } get { return _preferSystemChromeEnabled; }
set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } set { RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); }
} }
public double TitleBarHeight public double TitleBarHeight
{ {
get { return _titleBarHeight; } get { return _titleBarHeight; }
set { this.RaiseAndSetIfChanged(ref _titleBarHeight, value); } set { RaiseAndSetIfChanged(ref _titleBarHeight, value); }
} }
public WindowState WindowState public WindowState WindowState
{ {
get { return _windowState; } get { return _windowState; }
set { this.RaiseAndSetIfChanged(ref _windowState, value); } set { RaiseAndSetIfChanged(ref _windowState, value); }
} }
public WindowState[] WindowStates public WindowState[] WindowStates
{ {
get { return _windowStates; } get { return _windowStates; }
set { this.RaiseAndSetIfChanged(ref _windowStates, value); } set { RaiseAndSetIfChanged(ref _windowStates, value); }
} }
public bool IsSystemBarVisible public bool IsSystemBarVisible
{ {
get { return _isSystemBarVisible; } get { return _isSystemBarVisible; }
set { this.RaiseAndSetIfChanged(ref _isSystemBarVisible, value); } set { RaiseAndSetIfChanged(ref _isSystemBarVisible, value); }
} }
public bool DisplayEdgeToEdge public bool DisplayEdgeToEdge
{ {
get { return _displayEdgeToEdge; } get { return _displayEdgeToEdge; }
set { this.RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); } set { RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); }
} }
public Thickness SafeAreaPadding public Thickness SafeAreaPadding
{ {
get { return _safeAreaPadding; } get { return _safeAreaPadding; }
set { this.RaiseAndSetIfChanged(ref _safeAreaPadding, value); } set { RaiseAndSetIfChanged(ref _safeAreaPadding, value); }
}
public bool CanResize
{
get { return _canResize; }
set { RaiseAndSetIfChanged(ref _canResize, value); }
}
public bool CanMinimize
{
get { return _canMinimize; }
set { RaiseAndSetIfChanged(ref _canMinimize, value); }
} }
public bool CanMaximize
{
get { return _canMaximize; }
set { RaiseAndSetIfChanged(ref _canMaximize, value); }
}
public MiniCommand AboutCommand { get; } public MiniCommand AboutCommand { get; }
public MiniCommand ExitCommand { get; } public MiniCommand ExitCommand { get; }
@ -144,7 +180,7 @@ namespace ControlCatalog.ViewModels
public DateTime? ValidatedDateExample public DateTime? ValidatedDateExample
{ {
get => _validatedDateExample; get => _validatedDateExample;
set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value); set => RaiseAndSetIfChanged(ref _validatedDateExample, value);
} }
} }
} }

6
samples/IntegrationTestApp/Pages/WindowPage.axaml

@ -30,14 +30,16 @@
</ComboBox> </ComboBox>
<CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox> <CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox>
<CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox> <CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
<CheckBox Name="ShowWindowCanMinimize" IsChecked="True">Can Minimize</CheckBox>
<CheckBox Name="ShowWindowCanMaximize" IsChecked="True">Can Maximize</CheckBox>
</StackPanel>
<StackPanel Grid.Column="2">
<Button Name="ShowWindow" Click="ShowWindow_Click">Show Window</Button> <Button Name="ShowWindow" Click="ShowWindow_Click">Show Window</Button>
<Button Name="SendToBack" Click="SendToBack_Click">Send to Back</Button> <Button Name="SendToBack" Click="SendToBack_Click">Send to Back</Button>
<Button Name="EnterFullscreen" Click="EnterFullscreen_Click">Enter Fullscreen</Button> <Button Name="EnterFullscreen" Click="EnterFullscreen_Click">Enter Fullscreen</Button>
<Button Name="ExitFullscreen" Click="ExitFullscreen_Click">Exit Fullscreen</Button> <Button Name="ExitFullscreen" Click="ExitFullscreen_Click">Exit Fullscreen</Button>
<Button Name="RestoreAll" Click="RestoreAll_Click">Restore All</Button> <Button Name="RestoreAll" Click="RestoreAll_Click">Restore All</Button>
<Button Name="ShowTopmostWindow" Click="ShowTopmostWindow_Click">Show Topmost Window</Button> <Button Name="ShowTopmostWindow" Click="ShowTopmostWindow_Click">Show Topmost Window</Button>
</StackPanel>
<StackPanel Grid.Column="2">
<Button Name="ShowTransparentWindow" Click="ShowTransparentWindow_Click">Transparent Window</Button> <Button Name="ShowTransparentWindow" Click="ShowTransparentWindow_Click">Transparent Window</Button>
<Button Name="ShowTransparentPopup" Click="ShowTransparentPopup_Click">Transparent Popup</Button> <Button Name="ShowTransparentPopup" Click="ShowTransparentPopup_Click">Transparent Popup</Button>
</StackPanel> </StackPanel>

5
samples/IntegrationTestApp/Pages/WindowPage.axaml.cs

@ -23,10 +23,13 @@ public partial class WindowPage : UserControl
private void ShowWindow_Click(object? sender, RoutedEventArgs e) private void ShowWindow_Click(object? sender, RoutedEventArgs e)
{ {
var size = !string.IsNullOrWhiteSpace(ShowWindowSize.Text) ? Size.Parse(ShowWindowSize.Text) : (Size?)null; var size = !string.IsNullOrWhiteSpace(ShowWindowSize.Text) ? Size.Parse(ShowWindowSize.Text) : (Size?)null;
var canResize = ShowWindowCanResize.IsChecked ?? false;
var window = new ShowWindowTest var window = new ShowWindowTest
{ {
WindowStartupLocation = (WindowStartupLocation)ShowWindowLocation.SelectedIndex, WindowStartupLocation = (WindowStartupLocation)ShowWindowLocation.SelectedIndex,
CanResize = ShowWindowCanResize.IsChecked ?? false, CanResize = canResize,
CanMinimize = ShowWindowCanMinimize.IsChecked ?? false,
CanMaximize = canResize && (ShowWindowCanMaximize.IsChecked ?? false)
}; };
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)

55
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@ -21,6 +21,8 @@ namespace Avalonia.Controls.Chrome
internal const string PART_FullScreenButton = "PART_FullScreenButton"; internal const string PART_FullScreenButton = "PART_FullScreenButton";
private Button? _restoreButton; private Button? _restoreButton;
private Button? _minimizeButton;
private Button? _fullScreenButton;
private IDisposable? _disposables; private IDisposable? _disposables;
/// <summary> /// <summary>
@ -36,11 +38,16 @@ namespace Avalonia.Controls.Chrome
_disposables = new CompositeDisposable _disposables = new CompositeDisposable
{ {
HostWindow.GetObservable(Window.CanResizeProperty) HostWindow.GetObservable(Window.CanMaximizeProperty)
.Subscribe(x => .Subscribe(_ =>
{
UpdateRestoreButtonState();
UpdateFullScreenButtonState();
}),
HostWindow.GetObservable(Window.CanMinimizeProperty)
.Subscribe(_ =>
{ {
if (_restoreButton is not null) UpdateMinimizeButtonState();
_restoreButton.IsEnabled = x;
}), }),
HostWindow.GetObservable(Window.WindowStateProperty) HostWindow.GetObservable(Window.WindowStateProperty)
.Subscribe(x => .Subscribe(x =>
@ -49,6 +56,9 @@ namespace Avalonia.Controls.Chrome
PseudoClasses.Set(":normal", x == WindowState.Normal); PseudoClasses.Set(":normal", x == WindowState.Normal);
PseudoClasses.Set(":maximized", x == WindowState.Maximized); PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
UpdateRestoreButtonState();
UpdateMinimizeButtonState();
UpdateFullScreenButtonState();
}), }),
}; };
} }
@ -116,8 +126,8 @@ namespace Avalonia.Controls.Chrome
OnRestore(); OnRestore();
args.Handled = true; args.Handled = true;
}; };
restoreButton.IsEnabled = HostWindow?.CanResize ?? true;
_restoreButton = restoreButton; _restoreButton = restoreButton;
UpdateRestoreButtonState();
} }
if (e.NameScope.Find<Button>(PART_MinimizeButton) is { } minimizeButton) if (e.NameScope.Find<Button>(PART_MinimizeButton) is { } minimizeButton)
@ -127,6 +137,8 @@ namespace Avalonia.Controls.Chrome
OnMinimize(); OnMinimize();
args.Handled = true; args.Handled = true;
}; };
_minimizeButton = minimizeButton;
UpdateMinimizeButtonState();
} }
if (e.NameScope.Find<Button>(PART_FullScreenButton) is { } fullScreenButton) if (e.NameScope.Find<Button>(PART_FullScreenButton) is { } fullScreenButton)
@ -136,7 +148,40 @@ namespace Avalonia.Controls.Chrome
OnToggleFullScreen(); OnToggleFullScreen();
args.Handled = true; args.Handled = true;
}; };
_fullScreenButton = fullScreenButton;
UpdateFullScreenButtonState();
} }
} }
private void UpdateRestoreButtonState()
{
if (_restoreButton is null)
return;
_restoreButton.IsEnabled = HostWindow?.WindowState switch
{
WindowState.Maximized or WindowState.FullScreen => HostWindow.CanResize,
WindowState.Normal => HostWindow.CanMaximize,
_ => true
};
}
private void UpdateMinimizeButtonState()
{
if (_minimizeButton is null)
return;
_minimizeButton.IsEnabled = HostWindow?.CanMinimize ?? true;
}
private void UpdateFullScreenButtonState()
{
if (_fullScreenButton is null)
return;
_fullScreenButton.IsEnabled = HostWindow?.WindowState == WindowState.FullScreen ?
HostWindow.CanResize :
HostWindow?.CanMaximize ?? true;
}
} }
} }

11
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -64,6 +64,16 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
void CanResize(bool value); void CanResize(bool value);
/// <summary>
/// Enables or disables minimizing the window.
/// </summary>
void SetCanMinimize(bool value);
/// <summary>
/// Enables or disables maximizing the window.
/// </summary>
void SetCanMaximize(bool value);
/// <summary> /// <summary>
/// Gets or sets a method called before the underlying implementation is destroyed. /// Gets or sets a method called before the underlying implementation is destroyed.
/// Return true to prevent the underlying implementation from closing. /// Return true to prevent the underlying implementation from closing.
@ -124,7 +134,6 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Minimum width of the window. /// Minimum width of the window.
/// </summary> /// </summary>
///
void SetMinMaxSize(Size minSize, Size maxSize); void SetMinMaxSize(Size minSize, Size maxSize);
/// <summary> /// <summary>

53
src/Avalonia.Controls/Window.cs

@ -181,9 +181,24 @@ namespace Avalonia.Controls
public static readonly StyledProperty<WindowStartupLocation> WindowStartupLocationProperty = public static readonly StyledProperty<WindowStartupLocation> WindowStartupLocationProperty =
AvaloniaProperty.Register<Window, WindowStartupLocation>(nameof(WindowStartupLocation)); AvaloniaProperty.Register<Window, WindowStartupLocation>(nameof(WindowStartupLocation));
/// <summary>
/// Defines the <see cref="CanResize"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanResizeProperty = public static readonly StyledProperty<bool> CanResizeProperty =
AvaloniaProperty.Register<Window, bool>(nameof(CanResize), true); AvaloniaProperty.Register<Window, bool>(nameof(CanResize), true);
/// <summary>
/// Defines the <see cref="CanMinimize"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanMinimizeProperty =
AvaloniaProperty.Register<Window, bool>(nameof(CanMinimize), true);
/// <summary>
/// Defines the <see cref="CanMaximize"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanMaximizeProperty =
AvaloniaProperty.Register<Window, bool>(nameof(CanMaximize), true, coerce: CoerceCanMaximize);
/// <summary> /// <summary>
/// Routed event that can be used for global tracking of window destruction /// Routed event that can be used for global tracking of window destruction
/// </summary> /// </summary>
@ -236,6 +251,8 @@ namespace Avalonia.Controls
CreatePlatformImplBinding(TitleProperty, title => PlatformImpl!.SetTitle(title)); CreatePlatformImplBinding(TitleProperty, title => PlatformImpl!.SetTitle(title));
CreatePlatformImplBinding(IconProperty, icon => PlatformImpl!.SetIcon((icon ?? s_defaultIcon.Value)?.PlatformImpl)); CreatePlatformImplBinding(IconProperty, icon => PlatformImpl!.SetIcon((icon ?? s_defaultIcon.Value)?.PlatformImpl));
CreatePlatformImplBinding(CanResizeProperty, canResize => PlatformImpl!.CanResize(canResize)); CreatePlatformImplBinding(CanResizeProperty, canResize => PlatformImpl!.CanResize(canResize));
CreatePlatformImplBinding(CanMinimizeProperty, canMinimize => PlatformImpl!.SetCanMinimize(canMinimize));
CreatePlatformImplBinding(CanMaximizeProperty, canMaximize => PlatformImpl!.SetCanMaximize(canMaximize));
CreatePlatformImplBinding(ShowInTaskbarProperty, show => PlatformImpl!.ShowTaskbarIcon(show)); CreatePlatformImplBinding(ShowInTaskbarProperty, show => PlatformImpl!.ShowTaskbarIcon(show));
CreatePlatformImplBinding(WindowStateProperty, state => PlatformImpl!.WindowState = state); CreatePlatformImplBinding(WindowStateProperty, state => PlatformImpl!.WindowState = state);
@ -407,6 +424,32 @@ namespace Avalonia.Controls
set => SetValue(CanResizeProperty, value); set => SetValue(CanResizeProperty, value);
} }
/// <summary>
/// Enables or disables minimizing the window.
/// </summary>
/// <remarks>
/// This property might be ignored by some window managers on Linux.
/// </remarks>
public bool CanMinimize
{
get => GetValue(CanMinimizeProperty);
set => SetValue(CanMinimizeProperty, value);
}
/// <summary>
/// Enables or disables maximizing the window.
/// </summary>
/// <remarks>
/// <para>When <see cref="CanResize"/> is false, this property is always false.</para>
/// <para>On macOS, setting this property to false also disables the full screen mode.</para>
/// <para>This property might be ignored by some window managers on Linux.</para>
/// </remarks>
public bool CanMaximize
{
get => GetValue(CanMaximizeProperty);
set => SetValue(CanMaximizeProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the icon of the window. /// Gets or sets the icon of the window.
/// </summary> /// </summary>
@ -1184,7 +1227,7 @@ namespace Avalonia.Controls
PlatformImpl?.SetSystemDecorations(typedNewValue); PlatformImpl?.SetSystemDecorations(typedNewValue);
} }
if (change.Property == OwnerProperty) else if (change.Property == OwnerProperty)
{ {
var oldParent = change.OldValue as Window; var oldParent = change.OldValue as Window;
var newParent = change.NewValue as Window; var newParent = change.NewValue as Window;
@ -1197,6 +1240,11 @@ namespace Avalonia.Controls
impl.SetParent(_showingAsDialog ? newParent?.PlatformImpl! : (newParent?.PlatformImpl ?? null)); impl.SetParent(_showingAsDialog ? newParent?.PlatformImpl! : (newParent?.PlatformImpl ?? null));
} }
} }
else if (change.Property == CanResizeProperty)
{
CoerceValue(CanMaximizeProperty);
}
} }
protected override AutomationPeer OnCreateAutomationPeer() protected override AutomationPeer OnCreateAutomationPeer()
@ -1217,5 +1265,8 @@ namespace Avalonia.Controls
} }
return null; return null;
} }
private static bool CoerceCanMaximize(AvaloniaObject target, bool value)
=> value && target is not Window { CanResize: false };
} }
} }

8
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -133,6 +133,14 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public void SetCanMinimize(bool value)
{
}
public void SetCanMaximize(bool value)
{
}
public void SetTopmost(bool value) public void SetTopmost(bool value)
{ {
} }

8
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -152,6 +152,14 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public void SetCanMinimize(bool value)
{
}
public void SetCanMaximize(bool value)
{
}
public void SetTopmost(bool value) public void SetTopmost(bool value)
{ {
} }

25
src/Avalonia.Native/WindowImpl.cs

@ -18,6 +18,7 @@ namespace Avalonia.Native
private DoubleClickHelper _doubleClickHelper; private DoubleClickHelper _doubleClickHelper;
private readonly ITopLevelNativeMenuExporter _nativeMenuExporter; private readonly ITopLevelNativeMenuExporter _nativeMenuExporter;
private bool _canResize = true; private bool _canResize = true;
private bool _canMaximize = true;
internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(factory) internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(factory)
{ {
@ -77,6 +78,17 @@ namespace Avalonia.Native
_native.SetCanResize(value.AsComBool()); _native.SetCanResize(value.AsComBool());
} }
public void SetCanMinimize(bool value)
{
_native.SetCanMinimize(value.AsComBool());
}
public void SetCanMaximize(bool value)
{
_canMaximize = value;
_native.SetCanMaximize(value.AsComBool());
}
public void SetSystemDecorations(Controls.SystemDecorations enabled) public void SetSystemDecorations(Controls.SystemDecorations enabled)
{ {
_native.SetDecorations((Interop.SystemDecorations)enabled); _native.SetDecorations((Interop.SystemDecorations)enabled);
@ -138,10 +150,17 @@ namespace Avalonia.Native
{ {
if (_doubleClickHelper.IsDoubleClick(e.Timestamp, e.Position)) if (_doubleClickHelper.IsDoubleClick(e.Timestamp, e.Position))
{ {
if (_canResize) switch (WindowState)
{ {
WindowState = WindowState is WindowState.Maximized or WindowState.FullScreen ? case WindowState.Maximized or WindowState.FullScreen
WindowState.Normal : WindowState.Maximized; when _canResize:
WindowState = WindowState.Normal;
break;
case WindowState.Normal
when _canMaximize:
WindowState = WindowState.Maximized;
break;
} }
} }
else else

2
src/Avalonia.Native/avn.idl

@ -772,6 +772,8 @@ interface IAvnWindow : IAvnWindowBase
{ {
HRESULT SetEnabled(bool enable); HRESULT SetEnabled(bool enable);
HRESULT SetCanResize(bool value); HRESULT SetCanResize(bool value);
HRESULT SetCanMinimize(bool value);
HRESULT SetCanMaximize(bool value);
HRESULT SetDecorations(SystemDecorations value); HRESULT SetDecorations(SystemDecorations value);
HRESULT SetTitle(char* utf8Title); HRESULT SetTitle(char* utf8Title);
HRESULT SetTitleBarColor(AvnColor color); HRESULT SetTitleBarColor(AvnColor color);

2
src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml

@ -112,7 +112,7 @@
<Style Selector="^:fullscreen /template/ Button#PART_MinimizeButton"> <Style Selector="^:fullscreen /template/ Button#PART_MinimizeButton">
<Setter Property="IsVisible" Value="False" /> <Setter Property="IsVisible" Value="False" />
</Style> </Style>
<Style Selector="^ /template/ Button#PART_RestoreButton:disabled"> <Style Selector="^ /template/ Button:disabled">
<Setter Property="Opacity" Value="0.2"/> <Setter Property="Opacity" Value="0.2"/>
</Style> </Style>
</ControlTheme> </ControlTheme>

38
src/Avalonia.X11/X11Window.cs

@ -316,22 +316,28 @@ namespace Avalonia.X11
|| _systemDecorations == SystemDecorations.None) || _systemDecorations == SystemDecorations.None)
decorations = 0; decorations = 0;
if (!_canResize || !IsEnabled) var isDisabled = !IsEnabled;
if (!_canResize || isDisabled)
{ {
functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize); functions &= ~MotifFunctions.Resize;
decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH); decorations &= ~MotifDecorations.ResizeH;
} }
if (!IsEnabled)
{
functions &= ~(MotifFunctions.Resize | MotifFunctions.Minimize);
UpdateSizeHints(null, true); if (!_canMinimize || isDisabled)
{
functions &= ~MotifFunctions.Minimize;
decorations &= ~MotifDecorations.Minimize;
} }
else
if (!_canMaximize || isDisabled)
{ {
UpdateSizeHints(null); functions &= ~MotifFunctions.Maximize;
decorations &= ~MotifDecorations.Maximize;
} }
UpdateSizeHints(null, isDisabled);
var hints = new MotifWmHints var hints = new MotifWmHints
{ {
flags = new IntPtr((int)(MotifFlags.Decorations | MotifFlags.Functions)), flags = new IntPtr((int)(MotifFlags.Decorations | MotifFlags.Functions)),
@ -857,6 +863,8 @@ namespace Avalonia.X11
private SystemDecorations _systemDecorations = SystemDecorations.Full; private SystemDecorations _systemDecorations = SystemDecorations.Full;
private bool _canResize = true; private bool _canResize = true;
private bool _canMinimize = true;
private bool _canMaximize = true;
private const int MaxWindowDimension = 100000; private const int MaxWindowDimension = 100000;
private (Size minSize, Size maxSize) _scaledMinMaxSize = private (Size minSize, Size maxSize) _scaledMinMaxSize =
@ -1162,6 +1170,18 @@ namespace Avalonia.X11
UpdateSizeHints(null); UpdateSizeHints(null);
} }
public void SetCanMinimize(bool value)
{
_canMinimize = value;
UpdateMotifHints();
}
public void SetCanMaximize(bool value)
{
_canMaximize = value;
UpdateMotifHints();
}
public void SetCursor(ICursorImpl? cursor) public void SetCursor(ICursorImpl? cursor)
{ {
if (cursor == null) if (cursor == null)

8
src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs

@ -165,6 +165,14 @@ namespace Avalonia.Headless
} }
public void SetCanMinimize(bool value)
{
}
public void SetCanMaximize(bool value)
{
}
public Func<WindowCloseReason, bool>? Closing { get; set; } public Func<WindowCloseReason, bool>? Closing { get; set; }
private class FramebufferProxy : ILockedFramebuffer private class FramebufferProxy : ILockedFramebuffer

2
src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs

@ -12,6 +12,8 @@ namespace Avalonia.Win32
{ {
ShowInTaskbar = false, ShowInTaskbar = false,
IsResizable = false, IsResizable = false,
IsMinimizable = false,
IsMaximizable = false,
Decorations = SystemDecorations.None Decorations = SystemDecorations.None
}; };
} }

2
src/Windows/Avalonia.Win32/PopupImpl.cs

@ -111,6 +111,8 @@ namespace Avalonia.Win32
{ {
ShowInTaskbar = false, ShowInTaskbar = false,
IsResizable = false, IsResizable = false,
IsMinimizable = false,
IsMaximizable = false,
Decorations = SystemDecorations.None, Decorations = SystemDecorations.None,
}; };

36
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -136,6 +136,8 @@ namespace Avalonia.Win32
{ {
ShowInTaskbar = false, ShowInTaskbar = false,
IsResizable = true, IsResizable = true,
IsMinimizable = true,
IsMaximizable = true,
Decorations = SystemDecorations.Full Decorations = SystemDecorations.Full
}; };
@ -858,6 +860,24 @@ namespace Avalonia.Win32
UpdateWindowProperties(newWindowProperties); UpdateWindowProperties(newWindowProperties);
} }
public void SetCanMinimize(bool value)
{
var newWindowProperties = _windowProperties;
newWindowProperties.IsMinimizable = value;
UpdateWindowProperties(newWindowProperties);
}
public void SetCanMaximize(bool value)
{
var newWindowProperties = _windowProperties;
newWindowProperties.IsMaximizable = value;
UpdateWindowProperties(newWindowProperties);
}
public void SetSystemDecorations(SystemDecorations value) public void SetSystemDecorations(SystemDecorations value)
{ {
var newWindowProperties = _windowProperties; var newWindowProperties = _windowProperties;
@ -1425,15 +1445,19 @@ namespace Avalonia.Win32
style |= WindowStyles.WS_VISIBLE; style |= WindowStyles.WS_VISIBLE;
if (newProperties.IsResizable || newProperties.WindowState == WindowState.Maximized) if (newProperties.IsResizable || newProperties.WindowState == WindowState.Maximized)
{
style |= WindowStyles.WS_THICKFRAME; style |= WindowStyles.WS_THICKFRAME;
style |= WindowStyles.WS_MAXIMIZEBOX;
}
else else
{
style &= ~WindowStyles.WS_THICKFRAME; style &= ~WindowStyles.WS_THICKFRAME;
if (newProperties.IsMinimizable)
style |= WindowStyles.WS_MINIMIZEBOX;
else
style &= ~WindowStyles.WS_MINIMIZEBOX;
if (newProperties.IsMaximizable || (newProperties.WindowState == WindowState.Maximized && newProperties.IsResizable))
style |= WindowStyles.WS_MAXIMIZEBOX;
else
style &= ~WindowStyles.WS_MAXIMIZEBOX; style &= ~WindowStyles.WS_MAXIMIZEBOX;
}
const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU | WindowStyles.WS_BORDER; const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU | WindowStyles.WS_BORDER;
@ -1656,6 +1680,8 @@ namespace Avalonia.Win32
{ {
public bool ShowInTaskbar; public bool ShowInTaskbar;
public bool IsResizable; public bool IsResizable;
public bool IsMinimizable;
public bool IsMaximizable;
public SystemDecorations Decorations; public SystemDecorations Decorations;
public bool IsFullScreen; public bool IsFullScreen;
public WindowState WindowState; public WindowState WindowState;

17
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -672,6 +672,23 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void CanMaximize_Should_Be_False_If_CanResize_Is_False()
{
var windowImpl = MockWindowingPlatform.CreateWindowMock();
using var app = UnitTestApplication.Start(TestServices.StyledWindow.With(
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)));
var window = new Window();
Assert.True(window.CanMaximize);
window.CanResize = false;
Assert.False(window.CanMaximize);
}
public class SizingTests : ScopedTestBase public class SizingTests : ScopedTestBase
{ {
[Fact] [Fact]

43
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@ -311,6 +311,37 @@ namespace Avalonia.IntegrationTests.Appium
} }
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Window_Minimize_Button_Enabled_Matches_CanMinimize(bool canMinimize)
{
using (OpenWindow(canMinimize: canMinimize))
{
var secondaryWindow = Session.GetWindowById("SecondaryWindow");
var windowChrome = secondaryWindow.GetSystemChromeButtons();
Assert.NotNull(windowChrome.Minimize);
Assert.Equal(canMinimize, windowChrome.Minimize!.Enabled);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Window_Maximize_Button_Enabled_Matches_CanMaximize(bool canMaximize)
{
using (OpenWindow(canMaximize: canMaximize))
{
var secondaryWindow = Session.GetWindowById("SecondaryWindow");
var windowChrome = secondaryWindow.GetSystemChromeButtons();
var maximizeButton = windowChrome.FullScreen ?? windowChrome.Maximize;
Assert.NotNull(maximizeButton);
Assert.Equal(canMaximize, maximizeButton.Enabled);
}
}
[Fact] [Fact]
public void Changing_SystemDecorations_Should_Not_Change_Frame_Size_And_Position() public void Changing_SystemDecorations_Should_Not_Change_Frame_Size_And_Position()
{ {
@ -452,13 +483,17 @@ namespace Avalonia.IntegrationTests.Appium
WindowStartupLocation location = WindowStartupLocation.Manual, WindowStartupLocation location = WindowStartupLocation.Manual,
WindowState state = Controls.WindowState.Normal, WindowState state = Controls.WindowState.Normal,
bool canResize = true, bool canResize = true,
bool extendClientArea = false) bool extendClientArea = false,
bool canMinimize = true,
bool canMaximize = true)
{ {
var sizeTextBox = Session.FindElementByAccessibilityId("ShowWindowSize"); var sizeTextBox = Session.FindElementByAccessibilityId("ShowWindowSize");
var modeComboBox = Session.FindElementByAccessibilityId("ShowWindowMode"); var modeComboBox = Session.FindElementByAccessibilityId("ShowWindowMode");
var locationComboBox = Session.FindElementByAccessibilityId("ShowWindowLocation"); var locationComboBox = Session.FindElementByAccessibilityId("ShowWindowLocation");
var stateComboBox = Session.FindElementByAccessibilityId("ShowWindowState"); var stateComboBox = Session.FindElementByAccessibilityId("ShowWindowState");
var canResizeCheckBox = Session.FindElementByAccessibilityId("ShowWindowCanResize"); var canResizeCheckBox = Session.FindElementByAccessibilityId("ShowWindowCanResize");
var canMinimizeCheckBox = Session.FindElementByAccessibilityId("ShowWindowCanMinimize");
var canMaximizeCheckBox = Session.FindElementByAccessibilityId("ShowWindowCanMaximize");
var showButton = Session.FindElementByAccessibilityId("ShowWindow"); var showButton = Session.FindElementByAccessibilityId("ShowWindow");
var extendClientAreaCheckBox = Session.FindElementByAccessibilityId("ShowWindowExtendClientAreaToDecorationsHint"); var extendClientAreaCheckBox = Session.FindElementByAccessibilityId("ShowWindowExtendClientAreaToDecorationsHint");
@ -486,6 +521,12 @@ namespace Avalonia.IntegrationTests.Appium
if (canResizeCheckBox.GetIsChecked() != canResize) if (canResizeCheckBox.GetIsChecked() != canResize)
canResizeCheckBox.Click(); canResizeCheckBox.Click();
if (canMinimizeCheckBox.GetIsChecked() != canMinimize)
canMinimizeCheckBox.Click();
if (canMaximizeCheckBox.GetIsChecked() != canMaximize)
canMaximizeCheckBox.Click();
if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea) if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea)
extendClientAreaCheckBox.Click(); extendClientAreaCheckBox.Click();

Loading…
Cancel
Save