Browse Source

Expose allowed titlebar button actions from the platform implementation and respect them from drawn decorations (#21035)

* Expose WM window action capabilities from X11.

* Hide titlebar buttons for unsupported actions

* Simplify subscription

* Explose NetSupported property from X11Globals.cs, so it's easier to make checks from the rest of the codebase

* shifts

* comma
pull/21051/head
Nikita Tsukanov 5 months ago
committed by GitHub
parent
commit
412dca3aae
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 53
      src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
  2. 10
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  3. 33
      src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs
  4. 16
      src/Avalonia.Controls/Window.cs
  5. 14
      src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml
  6. 14
      src/Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml
  7. 4
      src/Avalonia.X11/X11Atoms.cs
  8. 25
      src/Avalonia.X11/X11Globals.cs
  9. 30
      src/Avalonia.X11/X11Window.cs
  10. 3
      src/Avalonia.X11/X11WindowModes/DefaultWindowMode.cs

53
src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs

@ -1,6 +1,7 @@
using System;
using Avalonia.Automation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.LogicalTree;
@ -15,7 +16,8 @@ namespace Avalonia.Controls.Chrome;
/// TopLevelHost extracts overlay/underlay/popover visuals from the template content
/// and inserts them into its own visual tree.
/// </summary>
[PseudoClasses(pcNormal, pcMaximized, pcFullscreen, pcHasShadow, pcHasBorder, pcHasTitlebar)]
[PseudoClasses(pcNormal, pcMaximized, pcFullscreen, pcHasShadow, pcHasBorder, pcHasTitlebar,
pcHasMaximize, pcHasFullscreen, pcHasMinimize)]
[TemplatePart(PART_CloseButton, typeof(Button))]
[TemplatePart(PART_MinimizeButton, typeof(Button))]
[TemplatePart(PART_MaximizeButton, typeof(Button))]
@ -31,6 +33,9 @@ public class WindowDrawnDecorations : StyledElement
internal const string pcHasShadow = ":has-shadow";
internal const string pcHasBorder = ":has-border";
internal const string pcHasTitlebar = ":has-titlebar";
internal const string pcHasMaximize = ":has-maximize";
internal const string pcHasFullscreen = ":has-fullscreen";
internal const string pcHasMinimize = ":has-minimize";
// Template part names for caption buttons
internal const string PART_CloseButton = "PART_CloseButton";
@ -383,8 +388,11 @@ public class WindowDrawnDecorations : StyledElement
Detach();
_hostWindow = window;
window.AllowedWindowActionsChanged += OnAllowedWindowActionsChanged;
_windowSubscriptions = new CompositeDisposable
{
Disposable.Create(() => window.AllowedWindowActionsChanged -= OnAllowedWindowActionsChanged),
window.GetObservable(Window.TitleProperty)
.Subscribe(title => SetCurrentValue(TitleProperty, title)),
window.GetObservable(Window.CanMaximizeProperty)
@ -407,6 +415,7 @@ public class WindowDrawnDecorations : StyledElement
}),
};
UpdateAllowedActionsPseudoClasses();
UpdateMaximizeButtonState();
UpdateMinimizeButtonState();
UpdateFullScreenButtonState();
@ -547,32 +556,54 @@ public class WindowDrawnDecorations : StyledElement
e.Handled = true;
}
private PlatformAllowedWindowActions EffectiveAllowedActions =>
_hostWindow?.AllowedWindowActions ?? PlatformAllowedWindowActions.All;
private void UpdateMaximizeButtonState()
{
if (_maximizeButton == null)
return;
_maximizeButton.IsEnabled = _hostWindow?.WindowState switch
{
WindowState.Maximized or WindowState.FullScreen => _hostWindow.CanResize,
WindowState.Normal => _hostWindow.CanMaximize,
_ => true
};
_maximizeButton.IsEnabled = EffectiveAllowedActions.HasFlag(PlatformAllowedWindowActions.Maximize)
&& (_hostWindow?.WindowState switch
{
WindowState.Maximized or WindowState.FullScreen => _hostWindow.CanResize,
WindowState.Normal => _hostWindow.CanMaximize,
_ => true
});
}
private void UpdateMinimizeButtonState()
{
if (_minimizeButton == null)
return;
_minimizeButton.IsEnabled = _hostWindow?.CanMinimize ?? true;
_minimizeButton.IsEnabled = EffectiveAllowedActions.HasFlag(PlatformAllowedWindowActions.Minimize)
&& (_hostWindow?.CanMinimize ?? true);
}
private void UpdateFullScreenButtonState()
{
if (_fullScreenButton == null)
return;
_fullScreenButton.IsEnabled = _hostWindow?.WindowState == WindowState.FullScreen
? _hostWindow.CanResize
: _hostWindow?.CanMaximize ?? true;
_fullScreenButton.IsEnabled = EffectiveAllowedActions.HasFlag(PlatformAllowedWindowActions.Fullscreen)
&& (_hostWindow?.WindowState == WindowState.FullScreen
? _hostWindow.CanResize
: _hostWindow?.CanMaximize ?? true);
}
private void OnAllowedWindowActionsChanged(PlatformAllowedWindowActions actions)
{
UpdateAllowedActionsPseudoClasses();
UpdateMaximizeButtonState();
UpdateMinimizeButtonState();
UpdateFullScreenButtonState();
}
private void UpdateAllowedActionsPseudoClasses()
{
var actions = EffectiveAllowedActions;
PseudoClasses.Set(pcHasMaximize, actions.HasFlag(PlatformAllowedWindowActions.Maximize));
PseudoClasses.Set(pcHasFullscreen, actions.HasFlag(PlatformAllowedWindowActions.Fullscreen));
PseudoClasses.Set(pcHasMinimize, actions.HasFlag(PlatformAllowedWindowActions.Minimize));
}
private void UpdateEffectiveGeometry()

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

@ -162,5 +162,15 @@ namespace Avalonia.Platform
/// </summary>
/// <param name="titleBarHeight">-1 for platform default, otherwise the height in DIPs.</param>
void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight);
/// <summary>
/// Gets the window actions that the underlying platform currently allows.
/// </summary>
PlatformAllowedWindowActions AllowedWindowActions => PlatformAllowedWindowActions.All;
/// <summary>
/// Gets or sets a callback invoked when <see cref="AllowedWindowActions"/> changes.
/// </summary>
Action<PlatformAllowedWindowActions>? AllowedWindowActionsChanged { get => null; set { } }
}
}

33
src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Metadata;
namespace Avalonia.Controls.Platform;
/// <summary>
/// Flags indicating which window actions the underlying platform supports.
/// </summary>
[Flags, PrivateApi]
public enum PlatformAllowedWindowActions
{
None = 0,
/// <summary>
/// The underlying platform supports maximizing/unmaximizing windows.
/// </summary>
Maximize = 1 << 0,
/// <summary>
/// The underlying platform supports fullscreen mode.
/// </summary>
Fullscreen = 1 << 1,
/// <summary>
/// The underlying platform supports minimizing windows.
/// </summary>
Minimize = 1 << 2,
/// <summary>
/// All actions are supported (default when the underlying platform does not report capabilities).
/// </summary>
All = Maximize | Fullscreen | Minimize,
}

16
src/Avalonia.Controls/Window.cs

@ -220,6 +220,7 @@ namespace Avalonia.Controls
private bool _positionWasSet;
private bool _wasShownBefore;
private IDisposable? _modalSubscription;
private PlatformAllowedWindowActions _allowedWindowActions = PlatformAllowedWindowActions.All;
/// <summary>
/// Initializes static members of the <see cref="Window"/> class.
@ -250,6 +251,8 @@ namespace Avalonia.Controls
impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
impl.AllowedWindowActionsChanged = OnAllowedWindowActionsChanged;
_allowedWindowActions = impl.AllowedWindowActions;
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x =>
{
ResizePlatformImpl(x, WindowResizeReason.Application);
@ -485,6 +488,11 @@ namespace Avalonia.Controls
set => SetValue(CanMaximizeProperty, value);
}
/// <summary>
/// Gets the window actions currently allowed by the underlying platform.
/// </summary>
internal PlatformAllowedWindowActions AllowedWindowActions => _allowedWindowActions;
/// <summary>
/// Gets or sets the icon of the window.
/// </summary>
@ -673,6 +681,14 @@ namespace Avalonia.Controls
UpdateDrawnDecorationParts();
}
internal event Action<PlatformAllowedWindowActions>? AllowedWindowActionsChanged;
private void OnAllowedWindowActionsChanged(PlatformAllowedWindowActions actions)
{
_allowedWindowActions = actions;
AllowedWindowActionsChanged?.Invoke(actions);
}
private void ExtendClientAreaToDecorationsChanged(bool isExtended)
{
IsExtendedIntoWindowDecorations = isExtended;

14
src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml

@ -196,6 +196,20 @@
<Setter Property="Opacity" Value="0.2"/>
</Style>
<!-- Hide caption buttons when the platform does not support the action -->
<Style Selector="^:not(:has-minimize) /template/ Button#PART_MinimizeButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-maximize) /template/ Button#PART_MaximizeButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-fullscreen) /template/ Button#PART_FullScreenButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-fullscreen) /template/ Button#PART_PopoverFullScreenButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<!-- Fullscreen: hide overlay and titlebar (popover takes over) -->
<Style Selector="^:fullscreen /template/ Panel#PART_TitleTextPanel">
<Setter Property="IsVisible" Value="False" />

14
src/Avalonia.Themes.Simple/Controls/WindowDrawnDecorations.xaml

@ -202,6 +202,20 @@
<Setter Property="Opacity" Value="0.2"/>
</Style>
<!-- Hide caption buttons when the platform does not support the action -->
<Style Selector="^:not(:has-minimize) /template/ Button#PART_MinimizeButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-maximize) /template/ Button#PART_MaximizeButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-fullscreen) /template/ Button#PART_FullScreenButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="^:not(:has-fullscreen) /template/ Button#PART_PopoverFullScreenButton">
<Setter Property="IsVisible" Value="False"/>
</Style>
<!-- Fullscreen: hide overlay and titlebar (popover takes over) -->
<Style Selector="^:fullscreen /template/ Panel#PART_TitleTextPanel">
<Setter Property="IsVisible" Value="False" />

4
src/Avalonia.X11/X11Atoms.cs

@ -144,6 +144,10 @@ namespace Avalonia.X11
public IntPtr _NET_WM_WINDOW_TYPE;
public IntPtr _NET_WM_STATE;
public IntPtr _NET_WM_ALLOWED_ACTIONS;
public IntPtr _NET_WM_ACTION_MAXIMIZE_VERT;
public IntPtr _NET_WM_ACTION_MAXIMIZE_HORZ;
public IntPtr _NET_WM_ACTION_FULLSCREEN;
public IntPtr _NET_WM_ACTION_MINIMIZE;
public IntPtr _NET_WM_STRUT;
public IntPtr _NET_WM_STRUT_PARTIAL;
public IntPtr _NET_WM_ICON_GEOMETRY;

25
src/Avalonia.X11/X11Globals.cs

@ -17,6 +17,7 @@ namespace Avalonia.X11
private IntPtr _compositionAtomOwner;
private bool _isCompositionEnabled;
private WindowActivationTrackingMode _activationTrackingMode;
private IntPtr[]? _netSupported;
public event Action? WindowManagerChanged;
public event Action? CompositionChanged;
@ -24,6 +25,7 @@ namespace Avalonia.X11
public event Action? NetActiveWindowPropertyChanged;
public event Action? RootGeometryChangedChanged;
public event Action? WindowActivationTrackingModeChanged;
public event Action? NetSupportedChanged;
public enum WindowActivationTrackingMode
{
@ -105,6 +107,16 @@ namespace Avalonia.X11
}
}
public IntPtr[]? NetSupported
{
get => _netSupported;
private set
{
_netSupported = value;
NetSupportedChanged?.Invoke();
}
}
private IntPtr GetSupportingWmCheck(IntPtr window)
{
XGetWindowProperty(_x11.Display, _rootWindow, _x11.Atoms._NET_SUPPORTING_WM_CHECK,
@ -187,17 +199,15 @@ namespace Avalonia.X11
}
}
private WindowActivationTrackingMode GetWindowActivityTrackingMode(IntPtr wm)
private WindowActivationTrackingMode GetWindowActivityTrackingMode(IntPtr wm, IntPtr[]? supportedFeatures)
{
if (Environment.GetEnvironmentVariable("AVALONIA_DEBUG_FORCE_X11_ACTIVATION_TRACKING_MODE") is
{ } forcedModeString
&& Enum.TryParse<WindowActivationTrackingMode>(forcedModeString, true, out var forcedMode))
return forcedMode;
if (wm == IntPtr.Zero)
if (wm == IntPtr.Zero || supportedFeatures == null)
return WindowActivationTrackingMode.FocusEvents;
var supportedFeatures = XGetWindowPropertyAsIntPtrArray(_x11.Display, _x11.RootWindow,
_x11.Atoms._NET_SUPPORTED, _x11.Atoms.ATOM) ?? [];
if (supportedFeatures.Contains(_x11.Atoms._NET_WM_STATE_FOCUSED))
return WindowActivationTrackingMode._NET_WM_STATE_FOCUSED;
@ -211,8 +221,13 @@ namespace Avalonia.X11
private void OnNewWindowManager()
{
var wm = GetActiveWm();
var supportedFeatures = wm != IntPtr.Zero
? XGetWindowPropertyAsIntPtrArray(_x11.Display, _x11.RootWindow,
_x11.Atoms._NET_SUPPORTED, _x11.Atoms.ATOM)
: null;
WmName = GetWmName(wm);
ActivationTrackingMode = GetWindowActivityTrackingMode(wm);
ActivationTrackingMode = GetWindowActivityTrackingMode(wm, supportedFeatures);
NetSupported = supportedFeatures;
}
private void OnRootWindowEvent(ref XEvent ev)

30
src/Avalonia.X11/X11Window.cs

@ -240,6 +240,8 @@ namespace Avalonia.X11
_activationTracker = new(_platform, this);
_activationTracker.ActivationChanged += HandleActivation;
_platform.Globals.NetSupportedChanged += OnNetSupportedChanged;
CreateIC();
XFlush(_x11.Display);
@ -512,7 +514,34 @@ namespace Avalonia.X11
public Action<PixelPoint>? PositionChanged { get; set; }
public Action? LostFocus { get; set; }
public PlatformAllowedWindowActions AllowedWindowActions => GetAllowedActions(_platform.Globals.NetSupported);
public Action<PlatformAllowedWindowActions>? AllowedWindowActionsChanged { get; set; }
public Compositor Compositor => _platform.Compositor;
private PlatformAllowedWindowActions GetAllowedActions(IntPtr[]? netSupported)
{
if (netSupported == null)
return PlatformAllowedWindowActions.All;
var actions = PlatformAllowedWindowActions.None;
if (netSupported.Contains(_x11.Atoms._NET_WM_ACTION_MAXIMIZE_VERT)
&& netSupported.Contains(_x11.Atoms._NET_WM_ACTION_MAXIMIZE_HORZ))
actions |= PlatformAllowedWindowActions.Maximize;
if (netSupported.Contains(_x11.Atoms._NET_WM_ACTION_FULLSCREEN))
actions |= PlatformAllowedWindowActions.Fullscreen;
if (netSupported.Contains(_x11.Atoms._NET_WM_ACTION_MINIMIZE))
actions |= PlatformAllowedWindowActions.Minimize;
return actions;
}
private void OnNetSupportedChanged() =>
AllowedWindowActionsChanged?.Invoke(AllowedWindowActions);
private void OnEvent(ref XEvent ev)
{
@ -1132,6 +1161,7 @@ namespace Avalonia.X11
}
_platform.X11Screens.Changed -= OnScreensChanged;
_platform.Globals.NetSupportedChanged -= OnNetSupportedChanged;
if (_useRenderWindow && _renderHandle != IntPtr.Zero)
{

3
src/Avalonia.X11/X11WindowModes/DefaultWindowMode.cs

@ -1,4 +1,5 @@
using System;
using System.Linq;
namespace Avalonia.X11;
@ -9,7 +10,7 @@ partial class X11Window
{
public override void Activate()
{
if (X11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero)
if (Platform.Globals.NetSupported?.Contains(X11.Atoms._NET_ACTIVE_WINDOW) == true)
{
Window.SendNetWMMessage(X11.Atoms._NET_ACTIVE_WINDOW, (IntPtr)1, X11.LastActivityTimestamp,
IntPtr.Zero);

Loading…
Cancel
Save