diff --git a/docfx/articles/dock-adapters.md b/docfx/articles/dock-adapters.md index 7affeff8f..6411c69d0 100644 --- a/docfx/articles/dock-adapters.md +++ b/docfx/articles/dock-adapters.md @@ -4,7 +4,7 @@ Dock exposes a small set of helper classes that adapt the core interfaces to dif ## HostAdapter -`HostAdapter` implements `IHostAdapter` and bridges a dock window (`IDockWindow`) with a platform-specific host window (`IHostWindow`). It resolves the host window via `IFactory.GetHostWindow` when needed, pushes layout, title, size, and position into the host when presenting, and reads the current size and position back when saving. +`HostAdapter` implements `IHostAdapter` and bridges a dock window (`IDockWindow`) with a platform-specific host window (`IHostWindow`). It resolves the host window via `IFactory.GetHostWindow` when needed, pushes layout, title, size, position, and `WindowState` into the host when presenting, and reads the current size, position, and state back when saving. Typical usage looks like the following: @@ -13,7 +13,7 @@ var hostAdapter = new HostAdapter(dockWindow); hostAdapter.Present(isDialog: false); ``` -The adapter fetches the host window instance from `IFactory.GetHostWindow` if it has not already been assigned. Calling `Save` stores the last known position and dimensions so they can be restored later. `Exit` closes the host and clears the reference, and `SetActive` forwards the activation request to the host window. +The adapter fetches the host window instance from `IFactory.GetHostWindow` if it has not already been assigned. Calling `Save` stores the last known position, dimensions, and window state so they can be restored later. `Exit` closes the host and clears the reference, and `SetActive` forwards the activation request to the host window. ## NavigateAdapter diff --git a/docfx/articles/dock-enums.md b/docfx/articles/dock-enums.md index 4834a5334..73ee2e533 100644 --- a/docfx/articles/dock-enums.md +++ b/docfx/articles/dock-enums.md @@ -97,6 +97,17 @@ Indicates the current state of an MDI document window. | `Minimized` | Window is minimized. | | `Maximized` | Window fills the document area. | +## DockWindowState + +Indicates the current state of a floating dock window (`IDockWindow.WindowState`). + +| Value | Description | +| ----- | ----------- | +| `Normal` | Window is in its normal size and position. | +| `Minimized` | Window is minimized. | +| `Maximized` | Window is maximized. | +| `FullScreen` | Window uses full-screen presentation when supported. | + ## GripMode Determines how the grip element in tool chrome behaves (used by tool docks). diff --git a/docfx/articles/dock-serialization.md b/docfx/articles/dock-serialization.md index 209aba3ee..1b97e3842 100644 --- a/docfx/articles/dock-serialization.md +++ b/docfx/articles/dock-serialization.md @@ -134,7 +134,7 @@ DI-based construction, use the `Dock.Serializer.DockSerializer` overload that ac 2. Load the layout and call `_dockState.Restore` to restore content/templates if needed. 3. Handle the case where the layout file does not exist or fails to deserialize. -Following this pattern keeps the window arrangement consistent across sessions. +Following this pattern keeps the window arrangement consistent across sessions. Floating window bounds, title, and `WindowState` are serialized as part of `IDockWindow`. ## Dockable identifiers diff --git a/docfx/articles/dock-windows.md b/docfx/articles/dock-windows.md index fead4f63f..2ad27859b 100644 --- a/docfx/articles/dock-windows.md +++ b/docfx/articles/dock-windows.md @@ -20,7 +20,7 @@ public override IDockWindow? CreateWindowFrom(IDockable dockable) } ``` -Calling `FloatDockable` on the factory opens a dockable in a new window. The new `IDockWindow` is tracked by the root dock and stores its bounds and title so it can be serialized together with the layout. +Calling `FloatDockable` on the factory opens a dockable in a new window. The new `IDockWindow` is tracked by the root dock and stores its bounds, title, and `WindowState` so it can be serialized together with the layout. To control parent/owner relationships and modality at creation time, use the `DockWindowOptions` overloads: @@ -58,6 +58,7 @@ For setup details see the [Managed windows guide](dock-managed-windows-guide.md) | `Id` | `string` | Window identifier. | | `X`, `Y` | `double` | Window position. | | `Width`, `Height` | `double` | Window size. | +| `WindowState` | `DockWindowState` | Window state (`Normal`, `Minimized`, `Maximized`, `FullScreen`). | | `Topmost` | `bool` | Keeps the window on top when `true`. | | `Title` | `string` | Window title. | | `OwnerMode` | `DockWindowOwnerMode` | Owner resolution mode for the host window. | @@ -72,7 +73,7 @@ For setup details see the [Managed windows guide](dock-managed-windows-guide.md) | `OnMoveDragBegin()` | `bool` | Drag begin callback (return `false` to cancel). | | `OnMoveDrag()` | `void` | Drag in progress callback. | | `OnMoveDragEnd()` | `void` | Drag end callback. | -| `Save()` | `void` | Persist size/position into the model. | +| `Save()` | `void` | Persist size/position/window state into the model. | | `Present(bool)` | `void` | Show the window. | | `Exit()` | `void` | Close the window. | | `SetActive()` | `void` | Activate the window. | @@ -90,6 +91,7 @@ For setup details see the [Managed windows guide](dock-managed-windows-guide.md) | `Exit()` | `void` | Close the host window. | | `SetPosition/GetPosition` | `void` | Read/write the host position. | | `SetSize/GetSize` | `void` | Read/write the host size. | +| `SetWindowState/GetWindowState` | `void` / `DockWindowState` | Read/write the host window state. | | `SetTitle` | `void` | Update the host title. | | `SetLayout` | `void` | Assign the hosted layout. | | `SetActive` | `void` | Activate the host window. | diff --git a/src/Dock.Avalonia/Controls/HostWindow.axaml.cs b/src/Dock.Avalonia/Controls/HostWindow.axaml.cs index 404bce05b..e5039aa0d 100644 --- a/src/Dock.Avalonia/Controls/HostWindow.axaml.cs +++ b/src/Dock.Avalonia/Controls/HostWindow.axaml.cs @@ -29,6 +29,10 @@ public class HostWindow : Window, IHostWindow private List _chromeGrips = new(); private HostWindowTitleBar? _hostWindowTitleBar; private bool _mouseDown, _draggingWindow; + private double _normalX = double.NaN; + private double _normalY = double.NaN; + private double _normalWidth = double.NaN; + private double _normalHeight = double.NaN; /// /// Define property. @@ -214,6 +218,7 @@ private void HostWindow_PositionChanged(object? sender, PixelPointEventArgs e) { if (Window is { } && IsTracked) { + CaptureNormalBounds(); Window.Save(); if (_mouseDown) @@ -267,6 +272,7 @@ private void HostWindow_LayoutUpdated(object? sender, EventArgs e) { if (Window is { } && IsTracked) { + CaptureNormalBounds(); Window.Save(); } } @@ -375,6 +381,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { UpdatePseudoClasses(IsToolWindow, ToolChromeControlsWholeWindow, change.GetNewValue()); } + else if (change.Property == WindowStateProperty && Window is { } && IsTracked) + { + Window.Save(); + } } private void UpdatePseudoClasses(bool isToolWindow, bool toolChromeControlsWholeWindow, bool documentChromeControlsWholeWindow) @@ -659,12 +669,21 @@ public void SetPosition(double x, double y) if (!double.IsNaN(x) && !double.IsNaN(y)) { Position = new PixelPoint((int)x, (int)y); + _normalX = x; + _normalY = y; } } /// public void GetPosition(out double x, out double y) { + if (WindowState != WindowState.Normal && TryGetNormalBounds(out var normalX, out var normalY, out _, out _)) + { + x = normalX; + y = normalY; + return; + } + x = Position.X; y = Position.Y; } @@ -675,21 +694,42 @@ public void SetSize(double width, double height) if (!double.IsNaN(width)) { Width = width; + _normalWidth = width; } if (!double.IsNaN(height)) { Height = height; + _normalHeight = height; } } /// public void GetSize(out double width, out double height) { + if (WindowState != WindowState.Normal && TryGetNormalBounds(out _, out _, out var normalWidth, out var normalHeight)) + { + width = normalWidth; + height = normalHeight; + return; + } + width = Width; height = Height; } + /// + public void SetWindowState(DockWindowState windowState) + { + WindowState = DockWindowStateHelper.ToAvaloniaWindowState(windowState); + } + + /// + public DockWindowState GetWindowState() + { + return DockWindowStateHelper.ToDockWindowState(WindowState); + } + /// public void SetTitle(string? title) { @@ -707,6 +747,39 @@ public void SetLayout(IDock layout) DataContext = layout; } + private void CaptureNormalBounds() + { + if (WindowState != WindowState.Normal) + { + return; + } + + if (double.IsNaN(Width) || double.IsNaN(Height)) + { + return; + } + + _normalX = Position.X; + _normalY = Position.Y; + _normalWidth = Width; + _normalHeight = Height; + } + + private bool TryGetNormalBounds(out double x, out double y, out double width, out double height) + { + x = _normalX; + y = _normalY; + width = _normalWidth; + height = _normalHeight; + + return !double.IsNaN(x) + && !double.IsNaN(y) + && !double.IsNaN(width) + && !double.IsNaN(height) + && width > 0 + && height > 0; + } + void IHostWindow.SetActive() { if(WindowState == WindowState.Minimized) diff --git a/src/Dock.Avalonia/Controls/ManagedDockWindowDocument.cs b/src/Dock.Avalonia/Controls/ManagedDockWindowDocument.cs index d72fb356b..fd4ecf937 100644 --- a/src/Dock.Avalonia/Controls/ManagedDockWindowDocument.cs +++ b/src/Dock.Avalonia/Controls/ManagedDockWindowDocument.cs @@ -39,6 +39,7 @@ public ManagedDockWindowDocument(IDockWindow window) _window = window; Id = window.Id; Title = window.Title; + _mdiState = DockWindowStateHelper.ToMdiWindowState(window.WindowState); Context = window; AttachWindow(window); AttachLayout(window.Layout); @@ -171,6 +172,14 @@ protected override void OnPropertyChanged(string propertyName) { _window.Id = Id; } + else if (propertyName == nameof(MdiState)) + { + var mappedState = DockWindowStateHelper.ToDockWindowState(_mdiState); + if (!(_window.WindowState == DockWindowState.FullScreen && mappedState == DockWindowState.Normal)) + { + _window.WindowState = mappedState; + } + } } private void AttachWindow(IDockWindow? window) @@ -212,6 +221,13 @@ private void WindowPropertyChanged(object? sender, PropertyChangedEventArgs e) { UpdateTitleFromLayout(); } + + return; + } + + if (e.PropertyName == nameof(IDockWindow.WindowState) && _window is { } window) + { + MdiState = DockWindowStateHelper.ToMdiWindowState(window.WindowState); } } diff --git a/src/Dock.Avalonia/Controls/ManagedHostWindow.cs b/src/Dock.Avalonia/Controls/ManagedHostWindow.cs index 396db5165..e9f5dfc20 100644 --- a/src/Dock.Avalonia/Controls/ManagedHostWindow.cs +++ b/src/Dock.Avalonia/Controls/ManagedHostWindow.cs @@ -23,6 +23,7 @@ public sealed class ManagedHostWindow : IHostWindow private double _y; private double _width = 400; private double _height = 300; + private DockWindowState _windowState = DockWindowState.Normal; private bool _closed; private bool _lastCloseCanceled; @@ -81,11 +82,16 @@ public void Present(bool isDialog) _document = new ManagedDockWindowDocument(Window); _document.Title = _title ?? _document.Title; _document.MdiBounds = new DockRect(_x, _y, _width, _height); + _document.MdiState = DockWindowStateHelper.ToMdiWindowState(_windowState); if (_layout is { } root) { _document.Content = ManagedDockWindowDocumentContent.Create(root); } } + else + { + _document.MdiState = DockWindowStateHelper.ToMdiWindowState(_windowState); + } _dock.AddWindow(_document); Window.Host = this; @@ -211,6 +217,36 @@ public void GetSize(out double width, out double height) height = _height; } + /// + public void SetWindowState(DockWindowState windowState) + { + _windowState = windowState; + if (Window is { } window) + { + window.WindowState = windowState; + } + + if (_document is not null) + { + _document.MdiState = DockWindowStateHelper.ToMdiWindowState(windowState); + } + } + + /// + public DockWindowState GetWindowState() + { + if (_document is not null) + { + var mappedState = DockWindowStateHelper.ToDockWindowState(_document.MdiState); + if (!(_windowState == DockWindowState.FullScreen && mappedState == DockWindowState.Normal)) + { + _windowState = mappedState; + } + } + + return _windowState; + } + /// public void SetTitle(string? title) { @@ -236,6 +272,11 @@ public void SetActive() { if (_dock is { } && _document is { }) { + if (_document.MdiState == MdiWindowState.Minimized) + { + _document.MdiState = MdiWindowState.Normal; + } + _dock.ActiveDockable = _document; } } diff --git a/src/Dock.Avalonia/Internal/DockWindowStateHelper.cs b/src/Dock.Avalonia/Internal/DockWindowStateHelper.cs new file mode 100644 index 000000000..8673b28d8 --- /dev/null +++ b/src/Dock.Avalonia/Internal/DockWindowStateHelper.cs @@ -0,0 +1,51 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +using Avalonia.Controls; +using Dock.Model.Core; + +namespace Dock.Avalonia.Internal; + +internal static class DockWindowStateHelper +{ + public static DockWindowState ToDockWindowState(WindowState windowState) + { + return windowState switch + { + WindowState.Minimized => DockWindowState.Minimized, + WindowState.Maximized => DockWindowState.Maximized, + WindowState.FullScreen => DockWindowState.FullScreen, + _ => DockWindowState.Normal + }; + } + + public static WindowState ToAvaloniaWindowState(DockWindowState windowState) + { + return windowState switch + { + DockWindowState.Minimized => WindowState.Minimized, + DockWindowState.Maximized => WindowState.Maximized, + DockWindowState.FullScreen => WindowState.FullScreen, + _ => WindowState.Normal + }; + } + + public static DockWindowState ToDockWindowState(MdiWindowState mdiWindowState) + { + return mdiWindowState switch + { + MdiWindowState.Minimized => DockWindowState.Minimized, + MdiWindowState.Maximized => DockWindowState.Maximized, + _ => DockWindowState.Normal + }; + } + + public static MdiWindowState ToMdiWindowState(DockWindowState windowState) + { + return windowState switch + { + DockWindowState.Minimized => MdiWindowState.Minimized, + DockWindowState.Maximized => MdiWindowState.Maximized, + _ => MdiWindowState.Normal + }; + } +} diff --git a/src/Dock.Model.Avalonia/Core/DockWindow.cs b/src/Dock.Model.Avalonia/Core/DockWindow.cs index 57772ff5e..86c8b4b48 100644 --- a/src/Dock.Model.Avalonia/Core/DockWindow.cs +++ b/src/Dock.Model.Avalonia/Core/DockWindow.cs @@ -52,6 +52,12 @@ public class DockWindow : ReactiveBase, IDockWindow public static readonly DirectProperty TopmostProperty = AvaloniaProperty.RegisterDirect(nameof(Topmost), o => o.Topmost, (o, v) => o.Topmost = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty WindowStateProperty = + AvaloniaProperty.RegisterDirect(nameof(WindowState), o => o.WindowState, (o, v) => o.WindowState = v); + /// /// Defines the property. /// @@ -112,6 +118,7 @@ public class DockWindow : ReactiveBase, IDockWindow private double _y; private double _width; private double _height; + private DockWindowState _windowState; private bool _topmost; private string _title; private DockWindowOwnerMode _ownerMode; @@ -130,6 +137,7 @@ public DockWindow() { _id = nameof(IDockWindow); _title = nameof(IDockWindow); + _windowState = DockWindowState.Normal; _ownerMode = DockWindowOwnerMode.Default; _hostAdapter = new HostAdapter(this); } @@ -179,6 +187,15 @@ public double Height set => SetAndRaise(HeightProperty, ref _height, value); } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + [JsonPropertyName("WindowState")] + public DockWindowState WindowState + { + get => _windowState; + set => SetAndRaise(WindowStateProperty, ref _windowState, value); + } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] [JsonPropertyName("Topmost")] diff --git a/src/Dock.Model.Avalonia/Json/AvaloniaDockSerializer.cs b/src/Dock.Model.Avalonia/Json/AvaloniaDockSerializer.cs index ff2a75896..f61bd5877 100644 --- a/src/Dock.Model.Avalonia/Json/AvaloniaDockSerializer.cs +++ b/src/Dock.Model.Avalonia/Json/AvaloniaDockSerializer.cs @@ -688,6 +688,7 @@ public AvaloniaDockSerializer() "Y", "Width", "Height", + "WindowState", "Topmost", "Title", "OwnerMode", @@ -704,6 +705,7 @@ public AvaloniaDockSerializer() "Y", "Width", "Height", + "WindowState", "Topmost", "Title", "OwnerMode", diff --git a/src/Dock.Model.CaliburMicro/Core/DockWindow.cs b/src/Dock.Model.CaliburMicro/Core/DockWindow.cs index b18eae758..c2a929dc4 100644 --- a/src/Dock.Model.CaliburMicro/Core/DockWindow.cs +++ b/src/Dock.Model.CaliburMicro/Core/DockWindow.cs @@ -18,6 +18,7 @@ public class DockWindow : CaliburMicroBase, IDockWindow private double _y; private double _width; private double _height; + private DockWindowState _windowState = DockWindowState.Normal; private bool _topmost; private string _title = nameof(IDockWindow); private DockWindowOwnerMode _ownerMode; @@ -78,6 +79,14 @@ public double Height set => Set(ref _height, value); } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public DockWindowState WindowState + { + get => _windowState; + set => Set(ref _windowState, value); + } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] public bool Topmost diff --git a/src/Dock.Model.Inpc/Core/DockWindow.cs b/src/Dock.Model.Inpc/Core/DockWindow.cs index 372cb2e6f..6dceb6606 100644 --- a/src/Dock.Model.Inpc/Core/DockWindow.cs +++ b/src/Dock.Model.Inpc/Core/DockWindow.cs @@ -19,6 +19,7 @@ public class DockWindow : ReactiveBase, IDockWindow private double _y; private double _width; private double _height; + private DockWindowState _windowState; private bool _topmost; private string _title; private DockWindowOwnerMode _ownerMode; @@ -37,6 +38,7 @@ public DockWindow() { _id = nameof(IDockWindow); _title = nameof(IDockWindow); + _windowState = DockWindowState.Normal; _ownerMode = DockWindowOwnerMode.Default; _hostAdapter = new HostAdapter(this); } @@ -81,6 +83,14 @@ public double Height set => SetProperty(ref _height, value); } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public DockWindowState WindowState + { + get => _windowState; + set => SetProperty(ref _windowState, value); + } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] public bool Topmost diff --git a/src/Dock.Model.Mvvm/Core/DockWindow.cs b/src/Dock.Model.Mvvm/Core/DockWindow.cs index 8eab0e407..06cd9c966 100644 --- a/src/Dock.Model.Mvvm/Core/DockWindow.cs +++ b/src/Dock.Model.Mvvm/Core/DockWindow.cs @@ -18,6 +18,7 @@ public class DockWindow : ReactiveBase, IDockWindow private double _y; private double _width; private double _height; + private DockWindowState _windowState; private bool _topmost; private string _title; private DockWindowOwnerMode _ownerMode; @@ -36,6 +37,7 @@ public DockWindow() { _id = nameof(IDockWindow); _title = nameof(IDockWindow); + _windowState = DockWindowState.Normal; _ownerMode = DockWindowOwnerMode.Default; _hostAdapter = new HostAdapter(this); } @@ -80,6 +82,14 @@ public double Height set => SetProperty(ref _height, value); } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public DockWindowState WindowState + { + get => _windowState; + set => SetProperty(ref _windowState, value); + } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] public bool Topmost diff --git a/src/Dock.Model.Prism/Core/DockWindow.cs b/src/Dock.Model.Prism/Core/DockWindow.cs index f557db963..28b3be14d 100644 --- a/src/Dock.Model.Prism/Core/DockWindow.cs +++ b/src/Dock.Model.Prism/Core/DockWindow.cs @@ -18,6 +18,7 @@ public class DockWindow : ReactiveBase, IDockWindow private double _y; private double _width; private double _height; + private DockWindowState _windowState; private bool _topmost; private string _title; private DockWindowOwnerMode _ownerMode; @@ -36,6 +37,7 @@ public DockWindow() { _id = nameof(IDockWindow); _title = nameof(IDockWindow); + _windowState = DockWindowState.Normal; _ownerMode = DockWindowOwnerMode.Default; _hostAdapter = new HostAdapter(this); } @@ -80,6 +82,14 @@ public double Height set => SetProperty(ref _height, value); } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public DockWindowState WindowState + { + get => _windowState; + set => SetProperty(ref _windowState, value); + } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] public bool Topmost diff --git a/src/Dock.Model.ReactiveProperty/Core/DockWindow.cs b/src/Dock.Model.ReactiveProperty/Core/DockWindow.cs index f1252bb45..3bbf34c21 100644 --- a/src/Dock.Model.ReactiveProperty/Core/DockWindow.cs +++ b/src/Dock.Model.ReactiveProperty/Core/DockWindow.cs @@ -19,6 +19,7 @@ public class DockWindow : ReactiveBase, IDockWindow private double _y; private double _width; private double _height; + private DockWindowState _windowState; private bool _topmost; private string _title; private DockWindowOwnerMode _ownerMode; @@ -37,6 +38,7 @@ public DockWindow() { _id = nameof(IDockWindow); _title = nameof(IDockWindow); + _windowState = DockWindowState.Normal; _ownerMode = DockWindowOwnerMode.Default; _hostAdapter = new HostAdapter(this); } @@ -81,6 +83,14 @@ public double Height set => SetProperty(ref _height, value); } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public DockWindowState WindowState + { + get => _windowState; + set => SetProperty(ref _windowState, value); + } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] public bool Topmost diff --git a/src/Dock.Model.ReactiveUI/Core/DockWindow.cs b/src/Dock.Model.ReactiveUI/Core/DockWindow.cs index 0cd9fd41e..aff73b5eb 100644 --- a/src/Dock.Model.ReactiveUI/Core/DockWindow.cs +++ b/src/Dock.Model.ReactiveUI/Core/DockWindow.cs @@ -22,6 +22,7 @@ public DockWindow() { _id = nameof(IDockWindow); _title = nameof(IDockWindow); + _windowState = DockWindowState.Normal; OwnerMode = DockWindowOwnerMode.Default; _hostAdapter = new HostAdapter(this); } @@ -51,6 +52,11 @@ public DockWindow() [Reactive] public partial double Height { get; set; } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + [Reactive] + public partial DockWindowState WindowState { get; set; } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] [Reactive] diff --git a/src/Dock.Model/Adapters/HostAdapter.cs b/src/Dock.Model/Adapters/HostAdapter.cs index 299129a01..9b6270941 100644 --- a/src/Dock.Model/Adapters/HostAdapter.cs +++ b/src/Dock.Model/Adapters/HostAdapter.cs @@ -25,6 +25,8 @@ public void Save() { if (_window.Host is not null) { + _window.WindowState = _window.Host.GetWindowState(); + _window.Host.GetPosition(out var x, out var y); _window.X = x; _window.Y = y; @@ -56,6 +58,7 @@ public void Present(bool isDialog) { _window.Host.SetPosition(_window.X, _window.Y); _window.Host.SetSize(_window.Width, _window.Height); + _window.Host.SetWindowState(_window.WindowState); _window.Host.SetTitle(_window.Title); _window.Host.SetLayout(_window.Layout); _window.Host.IsTracked = true; diff --git a/src/Dock.Model/Core/DockWindowState.cs b/src/Dock.Model/Core/DockWindowState.cs new file mode 100644 index 000000000..d93952df8 --- /dev/null +++ b/src/Dock.Model/Core/DockWindowState.cs @@ -0,0 +1,30 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +namespace Dock.Model.Core; + +/// +/// Represents the presentation state of a floating dock window. +/// +public enum DockWindowState +{ + /// + /// Window is shown at its normal size and position. + /// + Normal, + + /// + /// Window is minimized. + /// + Minimized, + + /// + /// Window is maximized. + /// + Maximized, + + /// + /// Window is shown in full-screen mode. + /// + FullScreen +} diff --git a/src/Dock.Model/Core/IDockWindow.cs b/src/Dock.Model/Core/IDockWindow.cs index 82d0213e1..81b16745a 100644 --- a/src/Dock.Model/Core/IDockWindow.cs +++ b/src/Dock.Model/Core/IDockWindow.cs @@ -34,6 +34,11 @@ public interface IDockWindow /// double Height { get; set; } + /// + /// Gets or sets the current window state. + /// + DockWindowState WindowState { get; set; } + /// /// Gets or sets whether this window appears on top of all other windows. /// diff --git a/src/Dock.Model/Core/IHostWindow.cs b/src/Dock.Model/Core/IHostWindow.cs index aef8ce4d4..e43d86e4d 100644 --- a/src/Dock.Model/Core/IHostWindow.cs +++ b/src/Dock.Model/Core/IHostWindow.cs @@ -61,6 +61,18 @@ public interface IHostWindow /// The host height. void GetSize(out double width, out double height); + /// + /// Sets host window state. + /// + /// The host window state. + void SetWindowState(DockWindowState windowState); + + /// + /// Gets host window state. + /// + /// The host window state. + DockWindowState GetWindowState(); + /// /// Sets host title. /// diff --git a/src/Dock.Model/FluentExtensions.cs b/src/Dock.Model/FluentExtensions.cs index 41b6c102f..ee34cccf6 100644 --- a/src/Dock.Model/FluentExtensions.cs +++ b/src/Dock.Model/FluentExtensions.cs @@ -1081,6 +1081,13 @@ public static IRootDock WithWindows(this IRootDock dock, params IDockWindow[] wi /// The same window. public static IDockWindow WithSize(this IDockWindow window, double width, double height) { window.Width = width; window.Height = height; return window; } /// + /// Sets the window state. + /// + /// The dock window. + /// Window state value. + /// The same window. + public static IDockWindow WithWindowState(this IDockWindow window, DockWindowState windowState) { window.WindowState = windowState; return window; } + /// /// Sets whether the window is topmost. /// /// The dock window. diff --git a/tests/Dock.Avalonia.HeadlessTests/DockControlMainWindowTests.cs b/tests/Dock.Avalonia.HeadlessTests/DockControlMainWindowTests.cs index 556755016..627689ec4 100644 --- a/tests/Dock.Avalonia.HeadlessTests/DockControlMainWindowTests.cs +++ b/tests/Dock.Avalonia.HeadlessTests/DockControlMainWindowTests.cs @@ -24,6 +24,7 @@ public class TestHostWindow : Window, IHostWindow private bool _presented; private bool _exited; private double _x, _y, _width, _height; + private DockWindowState _windowState = DockWindowState.Normal; private string? _title; private IDock? _layout; private bool _activated; @@ -58,6 +59,7 @@ public IDockWindow? Window public double TestHeight => _height; public string? TestTitle => _title; public IDock? TestLayout => _layout; + public DockWindowState TestWindowState => _windowState; public void Present(bool isDialog) { @@ -98,6 +100,16 @@ public void GetSize(out double width, out double height) height = _height; } + public void SetWindowState(DockWindowState windowState) + { + _windowState = windowState; + } + + public DockWindowState GetWindowState() + { + return _windowState; + } + public void SetTitle(string? title) { _title = title; @@ -180,6 +192,18 @@ public void HostWindow_Position_And_Size_Methods_Work() Assert.Equal(600, height); } + [AvaloniaFact] + public void HostWindow_WindowState_Methods_Work() + { + var hostWindow = new TestHostWindow(); + + hostWindow.SetWindowState(DockWindowState.Maximized); + var state = hostWindow.GetWindowState(); + + Assert.Equal(DockWindowState.Maximized, state); + Assert.Equal(DockWindowState.Maximized, hostWindow.TestWindowState); + } + [AvaloniaFact] public void HostWindow_Title_Setting_Works() { @@ -383,4 +407,4 @@ public void DockControl_With_HostWindow_Integration_Test() Assert.Same(root.Window, hostWindow.Window); Assert.Contains(dockControl, factory.DockControls); } -} \ No newline at end of file +} diff --git a/tests/Dock.Avalonia.HeadlessTests/HostWindowMethodsTests.cs b/tests/Dock.Avalonia.HeadlessTests/HostWindowMethodsTests.cs index 600d55f8a..832824d3f 100644 --- a/tests/Dock.Avalonia.HeadlessTests/HostWindowMethodsTests.cs +++ b/tests/Dock.Avalonia.HeadlessTests/HostWindowMethodsTests.cs @@ -1,5 +1,6 @@ using Avalonia.Headless.XUnit; using Dock.Avalonia.Controls; +using Dock.Model.Core; using Xunit; namespace Dock.Avalonia.HeadlessTests; @@ -45,4 +46,14 @@ public void SetSize_Ignores_NaN() Assert.True(double.IsNaN(w)); Assert.True(double.IsNaN(h)); } + + [AvaloniaFact] + public void SetWindowState_And_GetWindowState_Work() + { + var window = new HostWindow(); + + window.SetWindowState(DockWindowState.Maximized); + + Assert.Equal(DockWindowState.Maximized, window.GetWindowState()); + } } diff --git a/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs b/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs index ead6c5347..f1c9a043a 100644 --- a/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs +++ b/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs @@ -1040,6 +1040,49 @@ public void ManagedHostWindow_SetPosition_And_Size_Ignore_NaN() Assert.Equal(150, height); } + [AvaloniaFact] + public void ManagedHostWindow_WindowState_Roundtrips_With_Window_Model() + { + var factory = new Factory(); + var (host, window, _) = CreateManagedWindow(factory); + var dock = ManagedWindowRegistry.GetOrCreateDock(factory); + var managedDocument = dock.VisibleDockables!.OfType() + .Single(document => ReferenceEquals(document.Window, window)); + + host.SetWindowState(DockWindowState.Maximized); + + Assert.Equal(DockWindowState.Maximized, host.GetWindowState()); + Assert.Equal(DockWindowState.Maximized, window.WindowState); + Assert.Equal(MdiWindowState.Maximized, managedDocument.MdiState); + + managedDocument.MdiState = MdiWindowState.Minimized; + + Assert.Equal(DockWindowState.Minimized, window.WindowState); + Assert.Equal(DockWindowState.Minimized, host.GetWindowState()); + } + + [AvaloniaFact] + public void ManagedHostWindow_FullScreen_State_Is_Not_Lost_When_Mdi_Maps_To_Normal() + { + var factory = new Factory(); + var (host, window, _) = CreateManagedWindow(factory); + var dock = ManagedWindowRegistry.GetOrCreateDock(factory); + var managedDocument = dock.VisibleDockables!.OfType() + .Single(document => ReferenceEquals(document.Window, window)); + + managedDocument.MdiState = MdiWindowState.Maximized; + window.WindowState = DockWindowState.Maximized; + + host.SetWindowState(DockWindowState.FullScreen); + + Assert.Equal(MdiWindowState.Normal, managedDocument.MdiState); + Assert.Equal(DockWindowState.FullScreen, window.WindowState); + Assert.Equal(DockWindowState.FullScreen, host.GetWindowState()); + + window.Save(); + Assert.Equal(DockWindowState.FullScreen, window.WindowState); + } + [AvaloniaFact] public void ManagedHostWindow_SetLayout_Updates_Content() { @@ -1680,6 +1723,7 @@ private sealed class TestHostWindow : IHostWindow private double _y; private double _width; private double _height; + private DockWindowState _windowState = DockWindowState.Normal; public bool ExitCalled { get; private set; } @@ -1722,6 +1766,16 @@ public void GetSize(out double width, out double height) height = _height; } + public void SetWindowState(DockWindowState windowState) + { + _windowState = windowState; + } + + public DockWindowState GetWindowState() + { + return _windowState; + } + public void SetTitle(string? title) { } diff --git a/tests/Dock.Model.Avalonia.UnitTests/AvaloniaDockSerializerTests.cs b/tests/Dock.Model.Avalonia.UnitTests/AvaloniaDockSerializerTests.cs index a88b4abdb..6b4c52fa9 100644 --- a/tests/Dock.Model.Avalonia.UnitTests/AvaloniaDockSerializerTests.cs +++ b/tests/Dock.Model.Avalonia.UnitTests/AvaloniaDockSerializerTests.cs @@ -3,6 +3,7 @@ using Avalonia.Headless.XUnit; using Dock.Model.Avalonia; using Dock.Model.Avalonia.Controls; +using Dock.Model.Avalonia.Core; using Dock.Model.Avalonia.Json; using Dock.Model.Controls; using Dock.Model.Core; @@ -112,4 +113,22 @@ public void Deserialize_RootDock_RestoresPinnedDock() Assert.Equal(240, width, 3); Assert.Equal(180, height, 3); } + + [AvaloniaFact] + public void Serialize_DockWindow_IncludesWindowState() + { + var serializer = new AvaloniaDockSerializer(); + var window = new DockWindow + { + Id = "Window", + Title = "Window", + WindowState = DockWindowState.Maximized + }; + + var json = serializer.Serialize(window); + + using var document = JsonDocument.Parse(json); + Assert.True(document.RootElement.TryGetProperty("WindowState", out var value), json); + Assert.Equal((int)DockWindowState.Maximized, value.GetInt32()); + } } diff --git a/tests/Dock.Model.CaliburMicro.UnitTests/DockWindowTests.cs b/tests/Dock.Model.CaliburMicro.UnitTests/DockWindowTests.cs index b3c620405..1c7f0eb93 100644 --- a/tests/Dock.Model.CaliburMicro.UnitTests/DockWindowTests.cs +++ b/tests/Dock.Model.CaliburMicro.UnitTests/DockWindowTests.cs @@ -18,9 +18,11 @@ private class FakeHostWindow : IHostWindow public bool PresentCalled; public bool ExitCalled; public (double X,double Y)? SetPositionValue; public (double W,double H)? SetSizeValue; + public DockWindowState SetWindowStateValue { get; private set; } = DockWindowState.Normal; public string? Title; public IDock? Layout; public double GetX=1, GetY=2, GetW=3, GetH=4; + public DockWindowState GetWindowStateValue = DockWindowState.Normal; public void Present(bool isDialog) { PresentCalled = true; } public void Exit() { ExitCalled = true; } @@ -28,6 +30,8 @@ private class FakeHostWindow : IHostWindow public void GetPosition(out double x, out double y) { x=GetX; y=GetY; } public void SetSize(double width, double height) { SetSizeValue=(width,height); } public void GetSize(out double width, out double height) { width=GetW; height=GetH; } + public void SetWindowState(DockWindowState windowState) { SetWindowStateValue = windowState; } + public DockWindowState GetWindowState() { return GetWindowStateValue; } public void SetTitle(string? title) { Title = title; } public void SetLayout(IDock layout) { Layout = layout; } public void SetActive() { } @@ -36,7 +40,7 @@ public void SetActive() { } [Fact] public void Save_Stores_Host_Bounds() { - var host = new FakeHostWindow { GetX = 10, GetY = 20, GetW = 30, GetH = 40 }; + var host = new FakeHostWindow { GetX = 10, GetY = 20, GetW = 30, GetH = 40, GetWindowStateValue = DockWindowState.Maximized }; var window = new DockWindow { Host = host }; window.Save(); @@ -45,6 +49,7 @@ public void Save_Stores_Host_Bounds() Assert.Equal(20, window.Y); Assert.Equal(30, window.Width); Assert.Equal(40, window.Height); + Assert.Equal(DockWindowState.Maximized, window.WindowState); } [Fact] @@ -60,6 +65,7 @@ public void Present_Sends_State_To_Host() Y = 6, Width = 7, Height = 8, + WindowState = DockWindowState.FullScreen, Title = "test" }; @@ -69,6 +75,7 @@ public void Present_Sends_State_To_Host() Assert.True(host.IsTracked); Assert.Equal((5d,6d), host.SetPositionValue); Assert.Equal((7d,8d), host.SetSizeValue); + Assert.Equal(DockWindowState.FullScreen, host.SetWindowStateValue); Assert.Equal("test", host.Title); Assert.Same(root, host.Layout); } diff --git a/tests/Dock.Model.Prism.UnitTests/DockWindowTests.cs b/tests/Dock.Model.Prism.UnitTests/DockWindowTests.cs index a4af4eab7..07970b931 100644 --- a/tests/Dock.Model.Prism.UnitTests/DockWindowTests.cs +++ b/tests/Dock.Model.Prism.UnitTests/DockWindowTests.cs @@ -18,9 +18,11 @@ private class FakeHostWindow : IHostWindow public bool PresentCalled; public bool ExitCalled; public (double X,double Y)? SetPositionValue; public (double W,double H)? SetSizeValue; + public DockWindowState SetWindowStateValue { get; private set; } = DockWindowState.Normal; public string? Title; public IDock? Layout; public double GetX=1, GetY=2, GetW=3, GetH=4; + public DockWindowState GetWindowStateValue = DockWindowState.Normal; public void Present(bool isDialog) { PresentCalled = true; } public void Exit() { ExitCalled = true; } @@ -28,6 +30,8 @@ private class FakeHostWindow : IHostWindow public void GetPosition(out double x, out double y) { x=GetX; y=GetY; } public void SetSize(double width, double height) { SetSizeValue=(width,height); } public void GetSize(out double width, out double height) { width=GetW; height=GetH; } + public void SetWindowState(DockWindowState windowState) { SetWindowStateValue = windowState; } + public DockWindowState GetWindowState() { return GetWindowStateValue; } public void SetTitle(string? title) { Title = title; } public void SetLayout(IDock layout) { Layout = layout; } public void SetActive() { } @@ -36,7 +40,7 @@ public void SetActive() { } [Fact] public void Save_Stores_Host_Bounds() { - var host = new FakeHostWindow { GetX = 10, GetY = 20, GetW = 30, GetH = 40 }; + var host = new FakeHostWindow { GetX = 10, GetY = 20, GetW = 30, GetH = 40, GetWindowStateValue = DockWindowState.Maximized }; var window = new DockWindow { Host = host }; window.Save(); @@ -45,6 +49,7 @@ public void Save_Stores_Host_Bounds() Assert.Equal(20, window.Y); Assert.Equal(30, window.Width); Assert.Equal(40, window.Height); + Assert.Equal(DockWindowState.Maximized, window.WindowState); } [Fact] @@ -60,6 +65,7 @@ public void Present_Sends_State_To_Host() Y = 6, Width = 7, Height = 8, + WindowState = DockWindowState.FullScreen, Title = "test" }; @@ -69,6 +75,7 @@ public void Present_Sends_State_To_Host() Assert.True(host.IsTracked); Assert.Equal((5d,6d), host.SetPositionValue); Assert.Equal((7d,8d), host.SetSizeValue); + Assert.Equal(DockWindowState.FullScreen, host.SetWindowStateValue); Assert.Equal("test", host.Title); Assert.Same(root, host.Layout); } diff --git a/tests/Dock.Model.ReactiveUI.UnitTests/DockWindowOptionsTests.cs b/tests/Dock.Model.ReactiveUI.UnitTests/DockWindowOptionsTests.cs index 417cf3a01..92b74a850 100644 --- a/tests/Dock.Model.ReactiveUI.UnitTests/DockWindowOptionsTests.cs +++ b/tests/Dock.Model.ReactiveUI.UnitTests/DockWindowOptionsTests.cs @@ -14,6 +14,7 @@ private sealed class TestHostWindow : IHostWindow public IHostWindowState? HostWindowState { get; } public bool IsTracked { get; set; } public IDockWindow? Window { get; set; } + public DockWindowState WindowState { get; private set; } = DockWindowState.Normal; public bool PresentedAsDialog { get; private set; } public void Present(bool isDialog) @@ -45,6 +46,16 @@ public void GetSize(out double width, out double height) height = 0; } + public void SetWindowState(DockWindowState windowState) + { + WindowState = windowState; + } + + public DockWindowState GetWindowState() + { + return WindowState; + } + public void SetTitle(string? title) { } diff --git a/tests/Dock.Model.ReactiveUI.UnitTests/DockWindowTests.cs b/tests/Dock.Model.ReactiveUI.UnitTests/DockWindowTests.cs index f852daf49..978b4a1d3 100644 --- a/tests/Dock.Model.ReactiveUI.UnitTests/DockWindowTests.cs +++ b/tests/Dock.Model.ReactiveUI.UnitTests/DockWindowTests.cs @@ -21,6 +21,7 @@ private class TestHostWindow : IHostWindow public double Y { get; private set; } public double Width { get; private set; } public double Height { get; private set; } + public DockWindowState WindowState { get; private set; } public string? Title { get; private set; } public IDock? Layout { get; private set; } @@ -57,6 +58,16 @@ public void GetSize(out double width, out double height) width = Width; height = Height; } + public void SetWindowState(DockWindowState windowState) + { + WindowState = windowState; + } + + public DockWindowState GetWindowState() + { + return WindowState; + } + public void SetTitle(string? title) { Title = title; @@ -90,6 +101,7 @@ public void DockWindow_Defaults_Are_Correct() Assert.Equal(0, window.Y); Assert.Equal(0, window.Width); Assert.Equal(0, window.Height); + Assert.Equal(DockWindowState.Normal, window.WindowState); Assert.False(window.Topmost); Assert.Equal(DockWindowOwnerMode.Default, window.OwnerMode); Assert.Null(window.ParentWindow); @@ -105,6 +117,7 @@ public void Save_Updates_Position_And_Size_From_Host() window.Host = host; host.SetPosition(10, 20); host.SetSize(300, 200); + host.SetWindowState(DockWindowState.Maximized); window.Save(); @@ -112,13 +125,19 @@ public void Save_Updates_Position_And_Size_From_Host() Assert.Equal(20, window.Y); Assert.Equal(300, window.Width); Assert.Equal(200, window.Height); + Assert.Equal(DockWindowState.Maximized, window.WindowState); } [Fact] public void Present_Creates_Host_And_Presents() { var factory = new HostFactory(); - var window = new DockWindow { Factory = factory, Layout = new RootDock() }; + var window = new DockWindow + { + Factory = factory, + Layout = new RootDock(), + WindowState = DockWindowState.Maximized + }; window.Present(false); @@ -129,6 +148,7 @@ public void Present_Creates_Host_And_Presents() Assert.Same(window, host.Window); Assert.Equal(window.Title, host.Title); Assert.Equal(window.Layout, host.Layout); + Assert.Equal(DockWindowState.Maximized, host.WindowState); } [Fact] @@ -138,6 +158,7 @@ public void Exit_Saves_And_Resets_Host() var window = new DockWindow { Factory = factory, Layout = new RootDock(), Host = factory.Host }; factory.Host.SetPosition(5, 6); factory.Host.SetSize(100, 120); + factory.Host.SetWindowState(DockWindowState.FullScreen); window.Exit(); @@ -148,5 +169,6 @@ public void Exit_Saves_And_Resets_Host() Assert.Equal(6, window.Y); Assert.Equal(100, window.Width); Assert.Equal(120, window.Height); + Assert.Equal(DockWindowState.FullScreen, window.WindowState); } } diff --git a/tests/Dock.Serializer.UnitTests/DockLayoutRoundtripTests.cs b/tests/Dock.Serializer.UnitTests/DockLayoutRoundtripTests.cs index bb590eb0f..c1224d4a7 100644 --- a/tests/Dock.Serializer.UnitTests/DockLayoutRoundtripTests.cs +++ b/tests/Dock.Serializer.UnitTests/DockLayoutRoundtripTests.cs @@ -4,6 +4,7 @@ using Dock.Model.Core; using Dock.Model.Inpc; using Dock.Model.Inpc.Controls; +using Dock.Model.Inpc.Core; using Dock.Serializer.Protobuf; using Dock.Serializer.Xml; using Dock.Serializer.Yaml; @@ -94,7 +95,23 @@ private static RootDock CreateLayout() root.Title = "Root"; root.VisibleDockables = factory.CreateList(layout); root.ActiveDockable = layout; - root.Windows = factory.CreateList(); + root.Windows = factory.CreateList( + new DockWindow + { + Id = "Window1", + Title = "Window 1", + X = 120, + Y = 80, + Width = 640, + Height = 480, + WindowState = DockWindowState.Maximized, + Layout = new RootDock + { + Id = "FloatingRoot", + Title = "Floating Root", + VisibleDockables = factory.CreateList() + } + }); return root; } @@ -129,5 +146,18 @@ private static void AssertLayout(RootDock root) Assert.Single(documentDock.VisibleDockables!); Assert.Equal("Doc1", documentDock.VisibleDockables[0].Id); Assert.Equal("Doc1", documentDock.ActiveDockable?.Id); + + Assert.NotNull(root.Windows); + Assert.Single(root.Windows!); + var window = root.Windows![0]; + Assert.Equal("Window1", window.Id); + Assert.Equal("Window 1", window.Title); + Assert.Equal(120, window.X); + Assert.Equal(80, window.Y); + Assert.Equal(640, window.Width); + Assert.Equal(480, window.Height); + Assert.Equal(DockWindowState.Maximized, window.WindowState); + Assert.NotNull(window.Layout); + Assert.Equal("FloatingRoot", window.Layout!.Id); } }