diff --git a/Dock.slnx b/Dock.slnx index 97df6aa48..3466da765 100644 --- a/Dock.slnx +++ b/Dock.slnx @@ -58,6 +58,7 @@ + diff --git a/docfx/articles/README.md b/docfx/articles/README.md index 4ecb4e67c..12f33f353 100644 --- a/docfx/articles/README.md +++ b/docfx/articles/README.md @@ -35,6 +35,7 @@ Dock supports a wide range of UI patterns: - Tool panes that auto-hide, pin, or float. - Split layouts with proportional sizing and drag handles. - Floating windows with docking indicators. +- Managed floating windows hosted inside the main window. - Persisted layouts that restore across sessions. ## Key features diff --git a/docfx/articles/dock-floating-adorners.md b/docfx/articles/dock-floating-adorners.md index c30e897a2..4eace19bb 100644 --- a/docfx/articles/dock-floating-adorners.md +++ b/docfx/articles/dock-floating-adorners.md @@ -19,3 +19,5 @@ AppBuilder.Configure() When enabled `AdornerHelper` creates a lightweight `DockAdornerWindow` positioned above the drag source. The window is transparent and not topmost, so the drag preview window still appears above the adorner because only the preview is marked as `Topmost`. Use this option if dock targets fail to appear when dragging over native controls or popups. The default value is `false` which uses an `AdornerLayer` inside the same window. + +In managed window mode (`DockSettings.UseManagedWindows = true`), the floating adorner is rendered inside `ManagedWindowLayer` instead of a native window. diff --git a/docfx/articles/dock-host-window-locator.md b/docfx/articles/dock-host-window-locator.md index c48759429..8130bbb97 100644 --- a/docfx/articles/dock-host-window-locator.md +++ b/docfx/articles/dock-host-window-locator.md @@ -41,6 +41,21 @@ windows work without extra setup, but it also overwrites any locators you configured earlier. Disable `InitializeFactory` if you want to provide your own locators up front. +## Managed windows + +Dock can host floating windows inside the main window (managed mode). When +`DockSettings.UseManagedWindows` is enabled, the default locator returns a +`ManagedHostWindow` instead of a native `HostWindow`. You can still override the +locator or provide your own window factory via `DockControl.HostWindowFactory`. + +Managed windows also require `DockControl.EnableManagedWindowLayer` and a +`ManagedWindowLayer` template part. See the [Managed windows guide](dock-managed-windows-guide.md) +for setup details and [Managed windows reference](dock-managed-windows-reference.md) +for API information. + +In managed mode, floating windows are rendered using the MDI layout system, so +they remain part of the main visual tree and reuse the same theme resources. + ## Fallback locator If no entry matches the provided key, `GetHostWindow` calls diff --git a/docfx/articles/dock-managed-windows-guide.md b/docfx/articles/dock-managed-windows-guide.md new file mode 100644 index 000000000..f71604165 --- /dev/null +++ b/docfx/articles/dock-managed-windows-guide.md @@ -0,0 +1,49 @@ +# Managed Windows Guide + +Managed windows host floating docks inside the main window instead of spawning native OS windows. When enabled, Dock uses the in-app MDI layer to render floating windows as `MdiDocumentWindow` controls. + +## When to use managed windows + +Managed windows are useful when: + +- Your app should stay inside a single top-level window. +- You want consistent window chrome across platforms. +- Native window restrictions or airspace issues make OS windows undesirable. + +The main window is still an OS window. Managed windows affect only floating dock windows. + +## How managed windows work + +1. `DockSettings.UseManagedWindows` switches floating windows to `ManagedHostWindow`. +2. `DockControl` registers its `ManagedWindowLayer` with the factory when `EnableManagedWindowLayer` is true. +3. `ManagedHostWindow` creates a `ManagedDockWindowDocument` that wraps the `IDockWindow` model. +4. `ManagedWindowLayer` renders managed windows using `MdiLayoutPanel` and `MdiDocumentWindow`. + +The managed windows remain in the main visual tree so they can share styles and resources with the rest of the app. + +## Interaction parity + +Managed windows reuse the same interaction model as native windows: + +- Dragging and resizing are handled by `MdiDocumentWindow` and its layout manager. +- Dock targets, drag previews, and pinned windows are rendered inside the managed layer. +- Window magnetism and bring-to-front behavior apply within the managed layer. + +## Differences from native windows + +Managed windows are not OS windows: + +- They do not appear in the taskbar or window switchers. +- They cannot be moved outside the main window or across monitors. +- Owner relationships and OS-level z-order do not apply; ordering uses `MdiZIndex`. + +## Theming and templates + +`ManagedWindowLayer` is part of the `DockControl` template and must be named `PART_ManagedWindowLayer`. If you replace the template, include the layer and keep `EnableManagedWindowLayer` set to `true` so floating windows remain visible. + +## Related articles + +- [Managed windows how-to](dock-managed-windows-howto.md) +- [Managed windows reference](dock-managed-windows-reference.md) +- [Floating windows](dock-windows.md) +- [MDI document layout](dock-mdi.md) diff --git a/docfx/articles/dock-managed-windows-howto.md b/docfx/articles/dock-managed-windows-howto.md new file mode 100644 index 000000000..153b942c4 --- /dev/null +++ b/docfx/articles/dock-managed-windows-howto.md @@ -0,0 +1,75 @@ +# Managed Windows How-To + +This guide walks through enabling managed windows and wiring the managed window layer into your layout. + +## Enable managed windows + +Set the global flag before creating layouts: + +```csharp +using Dock.Settings; + +DockSettings.UseManagedWindows = true; +``` + +Or configure it via `AppBuilder`: + +```csharp +using Dock.Settings; + +AppBuilder.Configure() + .UsePlatformDetect() + .UseManagedWindows(); +``` + +## Ensure the managed layer is present + +The built-in Dock themes include a `ManagedWindowLayer` in the `DockControl` template. If you provide a custom template, keep the part and name it `PART_ManagedWindowLayer`. + +```xml + +``` + +Custom template snippet: + +```xml + +``` + +If you disable `EnableManagedWindowLayer`, managed floating windows will not appear. + +## Provide a managed host window (optional) + +If you override host window creation, return `ManagedHostWindow` when managed windows are enabled: + +```csharp +dockControl.HostWindowFactory = () => new ManagedHostWindow(); +``` + +You can also supply a factory mapping through `IFactory.HostWindowLocator` or `IFactory.DefaultHostWindowLocator`. + +## Customize managed window layout + +`ManagedWindowLayer` uses `ClassicMdiLayoutManager` by default. You can swap it with your own implementation: + +```csharp +managedWindowLayer.LayoutManager = new MyMdiLayoutManager(); +``` + +## Drag preview and overlays + +Drag previews and dock adorners are rendered inside the managed layer: + +```csharp +DockSettings.ShowDockablePreviewOnDrag = true; +DockSettings.DragPreviewOpacity = 0.7; +``` + +When dragging a managed floating window, the window itself moves so no preview overlay is shown. + +## Troubleshooting + +- Floating windows do not appear: verify `DockSettings.UseManagedWindows` is `true` and `EnableManagedWindowLayer` is enabled. +- Custom templates: ensure `PART_ManagedWindowLayer` exists in the `DockControl` template. +- Custom host window factory: return `ManagedHostWindow` when managed mode is enabled. diff --git a/docfx/articles/dock-managed-windows-reference.md b/docfx/articles/dock-managed-windows-reference.md new file mode 100644 index 000000000..0c80fe57e --- /dev/null +++ b/docfx/articles/dock-managed-windows-reference.md @@ -0,0 +1,46 @@ +# Managed Windows Reference + +This reference summarizes the main settings and types involved in managed window hosting. + +## Settings + +| Setting | Description | +| --- | --- | +| `DockSettings.UseManagedWindows` | Use in-app managed windows for floating dock windows. | +| `DockSettings.EnableWindowMagnetism` | Snap managed windows to nearby windows when dragging. | +| `DockSettings.WindowMagnetDistance` | Snap distance in pixels for managed window magnetism. | +| `DockSettings.BringWindowsToFrontOnDrag` | Activate managed windows when dragging begins. | +| `DockSettings.ShowDockablePreviewOnDrag` | Render dockable content inside drag previews. | +| `DockSettings.DragPreviewOpacity` | Opacity for drag previews in managed mode. | +| `DockSettings.UseOwnerForFloatingWindows` | Applies to native windows only. | + +## App builder and options + +| API | Description | +| --- | --- | +| `AppBuilderExtensions.UseManagedWindows` | Enables managed windows in the app builder. | +| `DockSettingsOptions.UseManagedWindows` | Nullable option used by `WithDockSettings`. | + +## DockControl integration + +| API | Description | +| --- | --- | +| `DockControl.EnableManagedWindowLayer` | Enables registration and display of the managed layer. | +| `DockControl.HostWindowFactory` | Override host window creation. Return `ManagedHostWindow` in managed mode. | + +## Managed window types + +| Type | Description | +| --- | --- | +| `ManagedHostWindow` | `IHostWindow` implementation that inserts a managed window into the layer. | +| `ManagedWindowLayer` | `TemplatedControl` that hosts managed windows and overlays. | +| `ManagedWindowDock` | Dock used to track managed floating windows. | +| `ManagedDockWindowDocument` | `IMdiDocument` wrapper around `IDockWindow` with `MdiBounds`, `MdiState`, and `MdiZIndex`. | + +## MDI integration + +| Type | Description | +| --- | --- | +| `MdiDocumentWindow` | Control that renders managed floating windows. | +| `IMdiLayoutManager` | Strategy for arranging MDI windows. | +| `ClassicMdiLayoutManager` | Default managed window layout manager. | diff --git a/docfx/articles/dock-mdi.md b/docfx/articles/dock-mdi.md index 85c84593c..8938b30db 100644 --- a/docfx/articles/dock-mdi.md +++ b/docfx/articles/dock-mdi.md @@ -64,5 +64,6 @@ For details on the built-in layout helpers and defaults see - The document header contains a drag handle that starts docking operations when you drag it outside the MDI surface. - When a document is maximized the stored bounds remain unchanged so restore returns to the previous size. +- Managed floating windows reuse `MdiDocumentWindow` inside `ManagedWindowLayer` when `DockSettings.UseManagedWindows` is enabled. See the [Managed windows guide](dock-managed-windows-guide.md). For an overview of all guides see the [documentation index](README.md). diff --git a/docfx/articles/dock-pinned-window.md b/docfx/articles/dock-pinned-window.md index 806c48016..bd2a1bbea 100644 --- a/docfx/articles/dock-pinned-window.md +++ b/docfx/articles/dock-pinned-window.md @@ -9,6 +9,8 @@ DockSettings.UsePinnedDockWindow = true; When enabled the `PinnedDockControl` places the preview content inside a lightweight `PinnedDockWindow`. The window follows the host layout and closes automatically when the tool is hidden. +In managed window mode (`DockSettings.UseManagedWindows = true`), pinned windows render inside `ManagedWindowLayer` instead of a native window. + ## Alignment and orientation `PinnedDockControl` exposes `PinnedDockAlignment` to control which edge the pinned preview occupies. The default templates bind it to the owning `ToolDock.Alignment`, but you can override it when building custom templates. diff --git a/docfx/articles/dock-settings.md b/docfx/articles/dock-settings.md index b7cd47112..b551ce0b3 100644 --- a/docfx/articles/dock-settings.md +++ b/docfx/articles/dock-settings.md @@ -97,6 +97,10 @@ The default value is `0.25`. `DockSettings.UsePinnedDockWindow` shows auto-hidden dockables inside a floating window instead of sliding panels. See [Pinned Dock Window](dock-pinned-window.md) for details. +## Managed windows + +`DockSettings.UseManagedWindows` hosts floating windows inside the main window using the managed window layer. This affects only floating docks; the main window remains native. See [Managed windows guide](dock-managed-windows-guide.md) for details. + ## Window magnetism `DockSettings.EnableWindowMagnetism` toggles snapping of floating windows. The snap distance @@ -173,6 +177,7 @@ AppBuilder.Configure() .UsePlatformDetect() .UseFloatingDockAdorner() .UseOwnerForFloatingWindows() + .UseManagedWindows() .EnableWindowMagnetism() .SetWindowMagnetDistance(16) .BringWindowsToFrontOnDrag() @@ -194,6 +199,7 @@ AppBuilder.Configure() | `MinimumVerticalDragDistance` | `DockSettings.MinimumVerticalDragDistance` | Vertical drag threshold. | | `UseFloatingDockAdorner` | `DockSettings.UseFloatingDockAdorner` | Floating adorner window. | | `UsePinnedDockWindow` | `DockSettings.UsePinnedDockWindow` | Floating pinned dock window. | +| `UseManagedWindows` | `DockSettings.UseManagedWindows` | Managed floating windows. | | `UseOwnerForFloatingWindows` | `DockSettings.UseOwnerForFloatingWindows` | Assign owners to floating windows. | | `EnableWindowMagnetism` | `DockSettings.EnableWindowMagnetism` | Snap floating windows. | | `WindowMagnetDistance` | `DockSettings.WindowMagnetDistance` | Snap distance in pixels. | diff --git a/docfx/articles/dock-window-creation-overrides.md b/docfx/articles/dock-window-creation-overrides.md new file mode 100644 index 000000000..1e63c46dd --- /dev/null +++ b/docfx/articles/dock-window-creation-overrides.md @@ -0,0 +1,265 @@ +# Window Creation Override Analysis + +## Scope and goals +This analysis focuses on every place the code base creates an Avalonia `Window` (or a derived type) and how those creation points can be overridden to support a managed window system or custom window implementations. The goal is to make all window creation paths consistent and easy to override, especially for floating dock windows created via factories. + +For practical setup guidance see the [Managed windows guide](dock-managed-windows-guide.md) and [Managed windows how-to](dock-managed-windows-howto.md). + +## Current window creation points + +### Library defaults +- `DockControl.InitializeFactory` assigns `HostWindowLocator` and `DefaultHostWindowLocator` to `new HostWindow()` when no factory is provided. This is the primary source of floating window creation for docking. (`src/Dock.Avalonia/Controls/DockControl.axaml.cs`) +- `HostAdapter.Present` lazily requests a host window via `Factory.GetHostWindow(id)` when an `IDockWindow` is shown. (`src/Dock.Model/Adapters/HostAdapter.cs`) +- The diagnostics helper creates a debug window directly via `new Window` and does not use a factory or locator. (`src/Dock.Avalonia.Diagnostics/Controls/RootDockDebugExtensions.cs`) + +### Internal helper windows (overlay/preview/pinned) +- `AdornerHelper` creates a floating `DockAdornerWindow` when `DockSettings.UseFloatingDockAdorner` is enabled. (`src/Dock.Avalonia/Internal/AdornerHelper.cs`, `src/Dock.Avalonia/Controls/DockAdornerWindow.axaml.cs`, `src/Dock.Settings/DockSettings.cs`) +- `DragPreviewHelper` creates and manages a singleton `DragPreviewWindow` for drag previews. (`src/Dock.Avalonia/Internal/DragPreviewHelper.cs`, `src/Dock.Avalonia/Controls/DragPreviewWindow.axaml.cs`) +- `PinnedDockControl` creates a `PinnedDockWindow` when `DockSettings.UsePinnedDockWindow` is enabled. (`src/Dock.Avalonia/Controls/PinnedDockControl.axaml.cs`, `src/Dock.Avalonia/Controls/PinnedDockWindow.axaml.cs`, `src/Dock.Settings/DockSettings.cs`) + +### App/sample entry points +- Code-only samples set `desktop.MainWindow = new Window { ... }`. (`samples/DockCodeOnlySample/Program.cs`, `samples/DockCodeOnlyMvvmSample/Program.cs`, `samples/DockFluentCodeOnlySample/Program.cs`) +- XAML samples declare `` roots and code-behind `MainWindow : Window` classes. (multiple `samples/**/MainWindow.axaml*`) + +### Model/window separation +- Floating window instances are `IDockWindow` (model objects) created by `IFactory.CreateDockWindow`. These are not UI `Window` instances but control the `IHostWindow` that is created later. (`src/Dock.Model.Avalonia/Factory.cs` and similar in other model packages) + +## Existing extension points +- `IFactory.HostWindowLocator` and `IFactory.DefaultHostWindowLocator` are the intended customization points for floating window creation. (`src/Dock.Model/FactoryBase.Locator.cs`) +- `FactoryBase.InitLayout` will create a default `HostWindowLocator` from `DefaultHostWindowLocator` if none was provided. (`src/Dock.Model/FactoryBase.Init.cs`) +- Custom `IHostWindow` implementations are supported, but most helpers assume an Avalonia `Window`/`TopLevel` underneath. + +## Coupling to Avalonia.Window +These call sites assume concrete `Window` behavior, which means custom host windows should be `Window`-derived (or provide equivalents) to keep features working: +- Window activation ordering uses `Window.SortWindowsByZOrder` and `Window.Activate`. (`src/Dock.Avalonia/Internal/WindowActivationHelper.cs`) +- Z-ordering for docking controls uses `Window.SortWindowsByZOrder`. (`src/Dock.Avalonia/Internal/DockHelpers.cs`) +- Controls expect `TopLevel.GetTopLevel(this)` to be an `IHostWindow` (so host windows must be `TopLevel` at minimum). (`src/Dock.Avalonia/Controls/DockControl.axaml.cs` and related controls) +- Window drag logic and chrome integrations are specialized for `HostWindow` and `Window`. (`src/Dock.Avalonia/Internal/WindowDragHelper.cs`, `src/Dock.Avalonia/Controls/ToolChromeControl.axaml.cs`, `src/Dock.Avalonia/Controls/DocumentTabStrip.axaml.cs`) +- Overlay resolution in Avalonia services explicitly checks `HostWindow` to resolve services from the DataContext. (`src/Dock.Model.ReactiveUI.Services.Avalonia/Services/AvaloniaHostServiceResolver.cs`) +- Pinned dock window hosting assumes a `Window` owner and attaches to `Window` events. (`src/Dock.Avalonia/Controls/PinnedDockControl.axaml.cs`) + +## How windows are used at runtime +This section maps the window lifecycle and the code paths that depend on a real OS `Window` vs a managed in-app window. + +### Lifecycle and tracking +- `IDockWindow` is a model object; `IHostWindow` is the runtime UI host. `HostAdapter.Present` creates/attaches the host and pushes layout, size, and title. (`src/Dock.Model/Adapters/HostAdapter.cs`) +- `HostWindow` registers itself in `Factory.HostWindows` on open/close and drives `WindowOpened/Closed` events. (`src/Dock.Avalonia/Controls/HostWindow.axaml.cs`) +- `DockControl` wires the root window to the root dock and raises window lifecycle events when the main window closes. (`src/Dock.Avalonia/Controls/DockControl.axaml.cs`) + +### Activation and z-order +- Global activation uses `Window.SortWindowsByZOrder` and `Window.Activate`. (`src/Dock.Avalonia/Internal/WindowActivationHelper.cs`) +- Dock control z-order computations assume OS windows and `Window.SortWindowsByZOrder`. (`src/Dock.Avalonia/Internal/DockHelpers.cs`) + +### Dragging, resizing, and chrome +- Floating window dragging uses `HostWindowState` + `BeginMoveDrag` and assumes a concrete `HostWindow`. (`src/Dock.Avalonia/Internal/HostWindowState.cs`, `src/Dock.Avalonia/Controls/HostWindow.axaml.cs`) +- Tool/document chrome controls attach to `HostWindow` and use OS drag behavior for Windows/macOS, with a `WindowDragHelper` fallback on Linux. (`src/Dock.Avalonia/Controls/ToolChromeControl.axaml.cs`, `src/Dock.Avalonia/Internal/WindowDragHelper.cs`) +- Managed MDI windows already include drag/resize/min/max logic within `MdiDocumentWindow`, which does not require an OS `Window`. (`src/Dock.Avalonia/Controls/MdiDocumentWindow.axaml.cs`) + +### Overlays, diagnostics, and helper windows +- Debug windows and overlay helpers create their own `Window` instances and rely on `TopLevel` to host overlays. (`src/Dock.Avalonia.Diagnostics/Controls/RootDockDebugExtensions.cs`, `src/Dock.Avalonia.Diagnostics/DockDebugOverlayManager.cs`) +- Floating adorner, drag preview, and pinned dock windows are implemented as OS windows. (`src/Dock.Avalonia/Internal/AdornerHelper.cs`, `src/Dock.Avalonia/Internal/DragPreviewHelper.cs`, `src/Dock.Avalonia/Controls/PinnedDockControl.axaml.cs`) + +### Service resolution and data context +- Overlay services resolve `HostWindow` and its `DataContext` explicitly. Managed windows must surface equivalent data for overlays/services. (`src/Dock.Model.ReactiveUI.Services.Avalonia/Services/AvaloniaHostServiceResolver.cs`) + +## Native vs managed window requirements +To ensure both OS windows and managed windows behave correctly, the same lifecycle contracts must be satisfied. + +### Required capabilities (both modes) +- **Lifecycle**: `Present`/`Exit` must add/remove the hosted layout and raise window events consistently. +- **Tracking**: size and position must flow between `IDockWindow` and the host. +- **Activation**: active window tracking and activation events must match dock state (`ActiveDockable`, focus). +- **Ownership**: floating windows should respect owner/main window ordering and modal behavior where applicable. + +### Native (OS) window specifics +- Use OS activation/z-order via `Window.SortWindowsByZOrder`, `Window.Activate`. +- Use OS move/resize via `BeginMoveDrag` and pointer capture. +- Use owner relationships for modal/float windows when `DockSettings.UseOwnerForFloatingWindows` is enabled. + +### Managed (in-app) window specifics +- Replace OS z-order with a managed ordering (e.g., `MdiZIndex`) and update it on activation. +- Replace OS drag/resize with MDI logic (already in `MdiDocumentWindow` + `IMdiLayoutManager`). +- Provide a managed “host layer” that can be queried for current window order and activation state. +- Bridge data context/service resolution (overlay services should resolve from managed host/root). +- Ensure managed window templates mirror native window themes (title bar, borders, shadows, resize grips, chrome buttons). +- Ensure input routing (pointer capture, drag handles, resize grips) mirrors native behavior so managed windows feel identical. + +## Gaps and friction points +- `DockControl.InitializeFactory` overwrites any `HostWindowLocator` or `DefaultHostWindowLocator` that might already be configured on the factory, making it hard to inject custom window types unless `InitializeFactory` is disabled. +- The diagnostics debug window is hardcoded as `new Window` and cannot be swapped without modifying code. +- Overlay/preview/pinned windows are hardcoded to `DockAdornerWindow`, `DragPreviewWindow`, and `PinnedDockWindow` with no factory hook. +- There is no single window factory abstraction shared across DockControl defaults, diagnostics, and other window creation sites. +- `HostWindowLocator` uses string IDs only; it cannot easily choose a window implementation based on richer context (e.g., tool window vs document window) unless IDs are manually encoded. +- Several runtime behaviors are hardcoded to OS windows (`HostWindowState`, z-order helpers, debug helpers, adorner/preview/pinned windows) and must be abstracted for managed windows. +- Overlay services and diagnostics assume `TopLevel`/`Window` roots, which do not exist for managed windows. + +## Proposed solutions + +### A. Add a host window factory hook to DockControl +- Introduce a property such as `Func HostWindowFactory` (or `Func HostWindowLocatorFactory`). +- In `InitializeFactory`, only assign `HostWindowLocator` or `DefaultHostWindowLocator` if they are null, and use `HostWindowFactory` when provided. +- Consider an overload that provides richer context, such as `Func`, so callers can map to different host window types without string IDs. + +### B. Centralize window creation via a shared factory service +- Add an `IWindowFactory` (or `IHostWindowFactory`) in `Dock.Avalonia` and register a default implementation that returns `HostWindow`. +- Use this factory in DockControl defaults and in diagnostics (`RootDockDebugExtensions`), with optional overrides via DI or explicit parameters. + +### C. Make diagnostics and helper windows overridable +- Add an overload to `AttachDockDebug` that accepts a `Func` or `Func`. +- Default to the current behavior when no delegate is provided, keeping compatibility. +- Provide similar factory hooks for `DockAdornerWindow`, `DragPreviewWindow`, and `PinnedDockWindow` (for example via `DockSettings`, a small `IWindowingServices` interface, or an `AppBuilder` extension). + +### D. Managed window implementation (reuse MDI) +Goal: allow floating windows to be hosted inside the main window (managed/MDI-style) instead of creating OS windows, while preserving existing `IDockWindow` + `IHostWindow` plumbing. + +Key idea: reuse the existing MDI system (`IMdiDocument`, `MdiLayoutPanel`, `IMdiLayoutManager`, `MdiDocumentWindow`) as the managed window surface, with a thin adapter so the `IHostWindow` contract can drive it. + +Proposed shape: +- Add a `ManagedHostWindow : IHostWindow` that does **not** derive from `Window`. It inserts a managed “window” control into an in-app layer and mirrors size/position/title to `IMdiDocument` state. +- Add a `ManagedWindowLayer` (or similar) control that hosts MDI windows inside the main window. This can be an `ItemsControl` + `MdiLayoutPanel` pair, reusing existing `MdiDocumentWindow` templates. +- Provide a factory switch (`DockSettings.UseManagedWindows` or `IFactory.WindowingMode`) that swaps `HostWindowLocator`/`DefaultHostWindowLocator` from `HostWindow` to `ManagedHostWindow`. +- Adapt the MDI window control to be less `IDock`-specific: + - Either introduce a small `IMdiHost` interface (ActiveItem/Items/InvalidateLayout) and use it instead of `IDock`. + - Or provide a lightweight `ManagedWindowDock : IDock` implementation in `Dock.Avalonia` for the managed layer, so existing `MdiDocumentWindow` logic keeps working without major changes. + +#### Managed window details (from analysis) +- **Reusable MDI components**: `IMdiDocument`, `MdiLayoutPanel`, `IMdiLayoutManager`, and `ClassicMdiLayoutManager` are already generic and can be reused as-is. (`src/Dock.Avalonia/Mdi/*`, `src/Dock.Model/Core/IMdiDocument.cs`) +- **Current coupling**: `MdiDocumentWindow` implements drag/resize/min/max logic, but assumes `IMdiDocument.Owner is IDock` and uses `IDock.ActiveDockable` for active state. (`src/Dock.Avalonia/Controls/MdiDocumentWindow.axaml.cs`) + +#### Refactors needed for reuse +- **Decouple host assumptions**: introduce a small host contract (for example `IMdiHost` with `ActiveItem`, `Items`, and `InvalidateLayout`) or provide an adapter dock used only by managed windows. +- **Managed window layer**: add a `ManagedWindowLayer` control (standalone or inside `DockControl`) to host managed windows inside the main window. +- **Managed host implementation**: create an `IHostWindow` implementation that inserts a managed window control into the layer rather than spawning an OS window. + +#### Minimal concrete approach +1. Create `ManagedWindowDock : IDock` (or a lighter `IMdiHost`) in `Dock.Avalonia` to own managed windows and track `ActiveDockable`. This allows `MdiDocumentWindow` to keep working with minimal changes. +2. Create `ManagedHostWindow : IHostWindow` that: + - Wraps a managed dock window model that implements `IMdiDocument`. + - On `Present`, inserts a `MdiDocumentWindow` into the `ManagedWindowLayer`. + - On `Exit`, removes the managed window from the layer. + - Maps `IDockWindow` bounds to `IMdiDocument.MdiBounds` (and back on save). +3. Add a factory hook to choose managed vs native windows: + - `HostWindowLocator` / `DefaultHostWindowLocator` returns `ManagedHostWindow` when managed mode is enabled. + - A `DockSettings` flag or `IFactory.WindowingMode` (`Native | Managed`) selects the path. + +#### Why this fits managed windows +- Managed windows render inside the main window (no OS window), and the MDI layout manager already provides drag/resize/min/max and z-ordering. +- The standard floating-window pipeline (`IDockWindow` + `IHostWindow`) remains intact, so app code does not need to change. + +## Detailed work required for native + managed parity +This list captures the additional refactors needed to ensure both window systems behave consistently. + +### Window lifecycle abstraction +- Introduce a small abstraction layer (for example `IWindowingServices`) that exposes: + - Create host window (native/managed) + - Activate window / bring to front + - Z-order query/update + - Optional owner/parent relationships +- Update `WindowActivationHelper` and `DockHelpers` to use this abstraction instead of `Window.SortWindowsByZOrder`. + +### Host state and drag handling +- Extend the existing `IHostWindowState` contract with a managed implementation (or provide a managed `HostWindowState` variant) so both native and managed hosts can drive the same docking events. +- For managed windows, reuse `MdiDocumentWindow` drag/resize handling and ensure it raises the same docking events (`WindowMoveDragBegin/Move/End`). + +### Overlay and diagnostics compatibility +- Refactor `AvaloniaHostServiceResolver` to resolve services from a managed host layer as well as `HostWindow`. +- Add factory hooks for debug/overlay windows to allow managed equivalents or in-tree overlays. + +### Helper windows +- Add explicit creation hooks for `DockAdornerWindow`, `DragPreviewWindow`, and `PinnedDockWindow`, and provide managed equivalents where appropriate. +- Ensure helper windows can attach to either a `TopLevel` or a managed host layer. +- Provide managed counterparts that can reuse the same styling resources as native windows. + +### Data context and layout ownership +- Managed host windows must expose a data context path for overlays and document tooling that currently assume `HostWindow`. +- Maintain the owner chain and active dockable in managed mode (either via `IMdiHost` or `ManagedWindowDock`). + +## Managed window theming and template parity +Managed windows should look and feel like real windows. The goal is to reuse existing window themes (Fluent/Simple) and chrome resources so managed substitutes are visually consistent and require minimal additional styling. + +### Theming goals +- Reuse existing `HostWindow` and window chrome resources where possible. +- Share title bar, button, and border templates between native and managed windows. +- Keep overlay/preview/pinned managed windows consistent with app themes. +- Support the same pseudo-classes (`:active`, `:maximized`, `:minimized`, `:dragging`) for consistent styling. + +### Template reuse strategy +- Extract common window chrome into reusable control themes or styles (for example `WindowChromeTheme`, `TitleBarTheme`, `WindowButtonTheme`) that can be applied by both `HostWindow` and managed window controls (or expose common brush keys that both templates consume). +- Ensure `MdiDocumentWindow` and managed helper windows (adorner/preview/pinned) use the same resource keys as `HostWindow` templates. +- Avoid duplicating theme XAML by referencing shared resources in `Dock.Avalonia.Themes.*` (Fluent/Simple). + +### Theme resource locations (current) +- `DockFluentTheme.axaml` includes window-related templates from `src/Dock.Avalonia.Themes.Fluent/Controls/*.axaml` (for example `HostWindow.axaml`, `HostWindowTitleBar.axaml`, `DragPreviewWindow.axaml`, `PinnedDockWindow.axaml`, `DockAdornerWindow.axaml`, `MdiDocumentWindow.axaml`, `ManagedWindowLayer.axaml`, `WindowChromeResources.axaml`). +- `DockSimpleTheme.axaml` includes the same `/Controls/*.axaml` resources; the Simple theme links Fluent control resources via `Dock.Avalonia.Themes.Simple.csproj` (see `AvaloniaResource` link to `Dock.Avalonia.Themes.Fluent/Controls`). +- New managed-window templates/resources should be added to `src/Dock.Avalonia.Themes.Fluent/Controls/` and referenced in both `DockFluentTheme.axaml` and `DockSimpleTheme.axaml`. + +### Managed substitutes that need templates +- **Managed host surface**: the managed window surface is the existing `MdiDocumentWindow` hosted by `ManagedWindowLayer`; no separate `ManagedHostWindowControl` is required unless future design calls for it. +- **Managed dock adorner overlay**: overlay-hosted `DockTarget`/`GlobalDockTarget` visuals should reuse the same brush keys as window chrome where applicable. +- **Managed drag preview host**: `DragPreviewControl` overlays should use the same theme resources as the native preview window. +- **Managed pinned dock host**: `ToolDockControl` overlays should use the same theme resources as the native pinned dock window. +- **Managed debug/diagnostic window** (optional): if added later, reuse the same window chrome resource keys for parity. + +### Theme integration checklist +- Define shared resource keys for window chrome (background, border, shadow, title bar, buttons). +- Apply those keys in both native `HostWindow` templates and managed window controls. +- Provide theme resources for managed helper windows in `Dock.Avalonia.Themes.Fluent` and `Dock.Avalonia.Themes.Simple`. +- Verify visual parity across light/dark variants and scaling. +- Verify pointer hit targets, padding, and resize grip thickness match native windows. + +## App and sample guidance +- Document a supported pattern for custom host windows: + - Set `Factory.DefaultHostWindowLocator = () => new MyHostWindow();` + - Disable `DockControl.InitializeFactory` if needed to avoid overrides. +- Provide at least one sample showing custom host windows (for example, a managed-window subclass or a themed host window). + +## Backward compatibility +- Preserve existing defaults when no custom factory/locator is supplied. +- Introduce new properties/overloads instead of changing existing signatures. + +## Suggested implementation steps +This is a concise summary. Use the checklist below as the source of truth for execution. + +1. Add a `HostWindowFactory` (or similar) property to `DockControl` and update `InitializeFactory` to respect existing locators and custom factories. +2. Add an optional factory parameter to `RootDockDebugExtensions.AttachDockDebug`. +3. Add creation hooks for `DockAdornerWindow`, `DragPreviewWindow`, and `PinnedDockWindow` (factory delegate or service). +4. Implement managed window hosting (MDI reuse) via `ManagedWindowLayer` + `ManagedHostWindow`. +5. Abstract activation/z-order/drag operations so managed windows can participate without `Window.SortWindowsByZOrder`. +6. Add managed-aware service resolution for overlays/diagnostics. +7. Extract shared chrome resources and apply them to managed window templates (host/adorner/preview/pinned/debug). +8. Update docs and samples to demonstrate managed windows and custom host windows with theme parity. + +## Implementation plan (checkable tasks) +1. [x] Add managed window mode flag (`DockSettings.UseManagedWindows` or `IFactory.WindowingMode`) and document default behavior. +2. [x] Create `ManagedWindowLayer` control to host MDI windows in-app. +3. [x] Add `ManagedHostWindow : IHostWindow` that maps `IDockWindow` to a managed MDI window item. +4. [x] Wire `HostWindowLocator`/`DefaultHostWindowLocator` to return `ManagedHostWindow` when managed mode is enabled. +5. [x] Update `MdiDocumentWindow` dependencies: + - Option A: introduce `IMdiHost` and refactor to use it. + - Option B: add a `ManagedWindowDock : IDock` implementation used by `ManagedWindowLayer`. +6. [x] Add factory hooks for `DockAdornerWindow`, `DragPreviewWindow`, and `PinnedDockWindow`. +7. [x] Add overloads/DI hooks for `RootDockDebugExtensions.AttachDockDebug`. +8. [x] Add abstraction for activation/z-order so managed windows can be ordered/activated without `Window.SortWindowsByZOrder`. +9. [x] Make overlay/service resolution work for managed windows (no `TopLevel`/`HostWindow`). +10. [x] Define shared window chrome resources in `src/Dock.Avalonia.Themes.Fluent/Controls/*.axaml` and include them in `DockFluentTheme.axaml`. +11. [x] Ensure `DockSimpleTheme.axaml` includes the same resources (linked from Fluent controls) and validate Simple theme overrides. +12. [x] Apply shared chrome resources to managed windows (host, adorner, drag preview, pinned dock, debug). +13. [x] Update docs (`dock-window-creation-overrides.md`, `dock-host-window-locator.md`) with managed window guidance and theming notes. +14. [x] Add/update a sample showing managed window hosting (MDI-backed floating windows) with theme parity. +15. [ ] Validate: drag/resize, activation, z-order, docking/undocking, theming consistency, input routing, and cleanup on window close. + +## Reference locations +- `src/Dock.Avalonia/Controls/DockControl.axaml.cs` +- `src/Dock.Model/FactoryBase.Locator.cs` +- `src/Dock.Model/FactoryBase.Init.cs` +- `src/Dock.Model/Adapters/HostAdapter.cs` +- `src/Dock.Avalonia.Diagnostics/Controls/RootDockDebugExtensions.cs` +- `src/Dock.Avalonia/Internal/AdornerHelper.cs` +- `src/Dock.Avalonia/Internal/DragPreviewHelper.cs` +- `src/Dock.Avalonia/Controls/PinnedDockControl.axaml.cs` +- `src/Dock.Avalonia/Internal/WindowActivationHelper.cs` +- `src/Dock.Avalonia/Internal/DockHelpers.cs` +- `src/Dock.Avalonia/Internal/WindowDragHelper.cs` +- `src/Dock.Model.ReactiveUI.Services.Avalonia/Services/AvaloniaHostServiceResolver.cs` +- `samples/*/Program.cs` +- `samples/**/MainWindow.axaml*` diff --git a/docfx/articles/dock-window-drag.md b/docfx/articles/dock-window-drag.md index 43eea6a80..0843b8e48 100644 --- a/docfx/articles/dock-window-drag.md +++ b/docfx/articles/dock-window-drag.md @@ -24,6 +24,10 @@ The same property is available on the Avalonia control `DocumentDock` and can al With the property enabled, `DocumentTabStrip` listens for pointer events on its background and initiates a window drag on the surrounding `HostWindow` (or window). The user can grab the tab area and drag the entire window. +## Managed windows + +In managed window mode (`DockSettings.UseManagedWindows = true`), floating windows are rendered inside the main window. Their drag behavior is provided by the managed window chrome (`MdiDocumentWindow`), not by `DocumentTabStrip`. `EnableWindowDrag` remains useful for dragging the main window or native floating windows. + ## Scenarios - **Floating windows** – Users can drag the tab bar of a floating document window to reposition it without relying on the window title bar. diff --git a/docfx/articles/dock-window-owner.md b/docfx/articles/dock-window-owner.md index 9895b4977..06074bfcf 100644 --- a/docfx/articles/dock-window-owner.md +++ b/docfx/articles/dock-window-owner.md @@ -17,3 +17,5 @@ AppBuilder.Configure() ``` When enabled Dock sets the main window as the owner for floating windows, preventing them from appearing behind it. Disable this if you want windows to be independent. + +Managed windows do not use OS-level owners, so this setting applies to native floating windows only. diff --git a/docfx/articles/dock-windows.md b/docfx/articles/dock-windows.md index 30fe67d49..8ac1a5c17 100644 --- a/docfx/articles/dock-windows.md +++ b/docfx/articles/dock-windows.md @@ -24,6 +24,14 @@ Calling `FloatDockable` on the factory opens a dockable in a new window. The new To customize the platform window (`IHostWindow`) used by floating docks, use `HostWindowLocator` or `DefaultHostWindowLocator`. See [Host window locators](dock-host-window-locator.md) for details. +## Managed windows + +When `DockSettings.UseManagedWindows` is enabled, floating windows are hosted inside the main window instead of spawning native OS windows. The default host window becomes `ManagedHostWindow`, which renders floating windows inside `ManagedWindowLayer` using the MDI layout system. + +If you override host window creation, return `ManagedHostWindow` when managed windows are enabled. `DockControl.EnableManagedWindowLayer` must remain `true` for managed windows to appear. + +For setup details see the [Managed windows guide](dock-managed-windows-guide.md) and [Managed windows how-to](dock-managed-windows-howto.md). + ## IDockWindow model members `IDockWindow` represents the floating window model and includes: @@ -98,12 +106,12 @@ This behavior is controlled by two settings on `DockSettings`: When magnetism is enabled, `HostWindow` compares its position against other tracked windows during a drag and adjusts the position if it falls within the snap distance. This makes it easy to align multiple floating -windows. +windows. In managed mode, the same logic applies to managed floating windows within the managed layer. ## Bringing windows to front If `DockSettings.BringWindowsToFrontOnDrag` is enabled, initiating a drag will activate all floating windows and any main window hosting a `DockControl` so they stay above other -applications until the drag completes. +applications until the drag completes. In managed mode, this updates managed z-order so windows stay above their peers. For more advanced scenarios see [Adapter Classes](dock-adapters.md) and the [Advanced Guide](dock-advanced.md). diff --git a/docfx/articles/toc.yml b/docfx/articles/toc.yml index 2532d709e..af2e3abd6 100644 --- a/docfx/articles/toc.yml +++ b/docfx/articles/toc.yml @@ -20,6 +20,8 @@ href: dock-control-initialization.md - name: Programmatic docking href: dock-programmatic-docking.md + - name: Managed windows how-to + href: dock-managed-windows-howto.md - name: DockManager guide href: dock-manager-guide.md - name: API scenarios @@ -36,6 +38,8 @@ href: dock-mdi.md - name: MDI layout helpers href: dock-mdi-layout-helpers.md + - name: Managed windows guide + href: dock-managed-windows-guide.md - name: Dock state guide href: dock-state.md - name: RestoreDockable behavior @@ -166,6 +170,8 @@ href: dock-layout-panels.md - name: Dock settings href: dock-settings.md + - name: Managed windows reference + href: dock-managed-windows-reference.md - name: Dock properties href: dock-properties.md - name: DockSettings in controls diff --git a/samples/DockCodeOnlySample/Program.cs b/samples/DockCodeOnlySample/Program.cs index 07ea7e856..8a0116d02 100644 --- a/samples/DockCodeOnlySample/Program.cs +++ b/samples/DockCodeOnlySample/Program.cs @@ -9,6 +9,7 @@ using Dock.Model.Avalonia; using Dock.Model.Avalonia.Controls; using Dock.Model.Core; +using Dock.Settings; namespace DockCodeOnlySample; @@ -23,7 +24,8 @@ static void Main(string[] args) static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .LogToTrace(); + .LogToTrace() + .UseManagedWindows(); } public class App : Application diff --git a/samples/DockInpcSample/ViewModels/DockFactory.cs b/samples/DockInpcSample/ViewModels/DockFactory.cs index 60b1c6d10..7679ee044 100644 --- a/samples/DockInpcSample/ViewModels/DockFactory.cs +++ b/samples/DockInpcSample/ViewModels/DockFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Dock.Avalonia.Controls; +using Dock.Settings; using Dock.Model.Controls; using Dock.Model.Core; using Dock.Model.Inpc; @@ -190,7 +191,7 @@ public override void InitLayout(IDockable layout) HostWindowLocator = new Dictionary> { - [nameof(IDockWindow)] = () => new HostWindow() + [nameof(IDockWindow)] = () => DockSettings.UseManagedWindows ? new ManagedHostWindow() : new HostWindow() }; base.InitLayout(layout); diff --git a/samples/DockMvvmSample/ViewModels/DockFactory.cs b/samples/DockMvvmSample/ViewModels/DockFactory.cs index cfd46d62d..f676a3fba 100644 --- a/samples/DockMvvmSample/ViewModels/DockFactory.cs +++ b/samples/DockMvvmSample/ViewModels/DockFactory.cs @@ -7,6 +7,7 @@ using DockMvvmSample.ViewModels.Tools; using DockMvvmSample.ViewModels.Views; using Dock.Avalonia.Controls; +using Dock.Settings; using Dock.Model.Controls; using Dock.Model.Core; using Dock.Model.Mvvm; @@ -197,7 +198,7 @@ public override void InitLayout(IDockable layout) HostWindowLocator = new Dictionary> { - [nameof(IDockWindow)] = () => new HostWindow() + [nameof(IDockWindow)] = () => DockSettings.UseManagedWindows ? new ManagedHostWindow() : new HostWindow() }; base.InitLayout(layout); diff --git a/samples/DockPrismSample/ViewModels/DockFactory.cs b/samples/DockPrismSample/ViewModels/DockFactory.cs index 7f1da0549..2c0b265a1 100644 --- a/samples/DockPrismSample/ViewModels/DockFactory.cs +++ b/samples/DockPrismSample/ViewModels/DockFactory.cs @@ -7,6 +7,7 @@ using DockPrismSample.ViewModels.Tools; using DockPrismSample.ViewModels.Views; using Dock.Avalonia.Controls; +using Dock.Settings; using Dock.Model.Controls; using Dock.Model.Core; using Dock.Model.Prism; @@ -210,7 +211,7 @@ public override void InitLayout(IDockable layout) HostWindowLocator = new Dictionary> { - [nameof(IDockWindow)] = () => new HostWindow() + [nameof(IDockWindow)] = () => DockSettings.UseManagedWindows ? new ManagedHostWindow() : new HostWindow() }; base.InitLayout(layout); diff --git a/samples/DockReactivePropertySample/ViewModels/DockFactory.cs b/samples/DockReactivePropertySample/ViewModels/DockFactory.cs index d953df06d..f14891b02 100644 --- a/samples/DockReactivePropertySample/ViewModels/DockFactory.cs +++ b/samples/DockReactivePropertySample/ViewModels/DockFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Dock.Avalonia.Controls; +using Dock.Settings; using Dock.Model.Controls; using Dock.Model.Core; using Dock.Model.ReactiveProperty; @@ -190,7 +191,7 @@ public override void InitLayout(IDockable layout) HostWindowLocator = new Dictionary> { - [nameof(IDockWindow)] = () => new HostWindow() + [nameof(IDockWindow)] = () => DockSettings.UseManagedWindows ? new ManagedHostWindow() : new HostWindow() }; base.InitLayout(layout); diff --git a/samples/DockReactiveUICanonicalSample/ViewModels/DockFactory.cs b/samples/DockReactiveUICanonicalSample/ViewModels/DockFactory.cs index 5dcf7b5dc..3494ae2c7 100644 --- a/samples/DockReactiveUICanonicalSample/ViewModels/DockFactory.cs +++ b/samples/DockReactiveUICanonicalSample/ViewModels/DockFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Dock.Avalonia.Controls; +using Dock.Settings; using Dock.Model.Controls; using Dock.Model.Core; using Dock.Model.ReactiveUI; @@ -140,7 +141,7 @@ public override void InitLayout(IDockable layout) { HostWindowLocator = new Dictionary> { - [nameof(IDockWindow)] = () => new HostWindow() + [nameof(IDockWindow)] = () => DockSettings.UseManagedWindows ? new ManagedHostWindow() : new HostWindow() }; base.InitLayout(layout); diff --git a/samples/DockReactiveUICanonicalSample/ViewModels/Workspace/WorkspaceDockFactory.cs b/samples/DockReactiveUICanonicalSample/ViewModels/Workspace/WorkspaceDockFactory.cs index 95f33aad9..38a7713e4 100644 --- a/samples/DockReactiveUICanonicalSample/ViewModels/Workspace/WorkspaceDockFactory.cs +++ b/samples/DockReactiveUICanonicalSample/ViewModels/Workspace/WorkspaceDockFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Dock.Avalonia.Controls; +using Dock.Settings; using Dock.Model.Controls; using Dock.Model.Core; using Dock.Model.ReactiveUI; @@ -43,7 +44,7 @@ public override void InitLayout(IDockable layout) { HostWindowLocator = new Dictionary> { - [nameof(IDockWindow)] = () => new HostWindow() + [nameof(IDockWindow)] = () => DockSettings.UseManagedWindows ? new ManagedHostWindow() : new HostWindow() }; base.InitLayout(layout); diff --git a/samples/DockReactiveUIManagedSample/App.axaml b/samples/DockReactiveUIManagedSample/App.axaml new file mode 100644 index 000000000..ff7b7befc --- /dev/null +++ b/samples/DockReactiveUIManagedSample/App.axaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + #EFEFEF + #EFEFEF + #EFEFEF + #212121 + #212121 + #33323232 + #33323232 + #FFFAFAFA + #E2E2E2 + + + #2F2F2F + #2F2F2F + #2F2F2F + #FFFFFF + #FFFFFF + #434343 + #434343 + #FF212121 + #1F1F1F + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/DockReactiveUIManagedSample/App.axaml.cs b/samples/DockReactiveUIManagedSample/App.axaml.cs new file mode 100644 index 000000000..bd96bb4b9 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/App.axaml.cs @@ -0,0 +1,79 @@ +using System.Diagnostics.CodeAnalysis; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Dock.Avalonia.Diagnostics.Controls; +using Dock.Avalonia.Diagnostics; +using DockReactiveUIManagedSample.Themes; +using DockReactiveUIManagedSample.ViewModels; +using DockReactiveUIManagedSample.Views; + +namespace DockReactiveUIManagedSample; + +[RequiresUnreferencedCode("Requires unreferenced code for MainWindowViewModel.")] +[RequiresDynamicCode("Requires unreferenced code for MainWindowViewModel.")] +public class App : Application +{ + public static IThemeManager? ThemeManager; + + public override void Initialize() + { + ThemeManager = new FluentThemeManager(); + + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + // DockManager.s_enableSplitToWindow = true; + + var mainWindowViewModel = new MainWindowViewModel(); + + switch (ApplicationLifetime) + { + case IClassicDesktopStyleApplicationLifetime desktopLifetime: + { + var mainWindow = new MainWindow + { + DataContext = mainWindowViewModel + }; +#if DEBUG + mainWindow.AttachDockDebug( + () => mainWindowViewModel.Layout!, + new KeyGesture(Key.F11)); + mainWindow.AttachDockDebugOverlay(new KeyGesture(Key.F9)); +#endif + mainWindow.Closing += (_, _) => + { + mainWindowViewModel.CloseLayout(); + }; + + desktopLifetime.MainWindow = mainWindow; + + desktopLifetime.Exit += (_, _) => + { + mainWindowViewModel.CloseLayout(); + }; + + break; + } + case ISingleViewApplicationLifetime singleViewLifetime: + { + var mainView = new MainView() + { + DataContext = mainWindowViewModel + }; + + singleViewLifetime.MainView = mainView; + + break; + } + } + + base.OnFrameworkInitializationCompleted(); +#if DEBUG + this.AttachDevTools(); +#endif + } +} diff --git a/samples/DockReactiveUIManagedSample/DockReactiveUIManagedSample.csproj b/samples/DockReactiveUIManagedSample/DockReactiveUIManagedSample.csproj new file mode 100644 index 000000000..1c541c8c5 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/DockReactiveUIManagedSample.csproj @@ -0,0 +1,41 @@ + + + + net10.0 + WinExe + False + False + enable + OnlyProperties + true + $(BaseIntermediateOutputPath)\GeneratedFiles + + + + true + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + diff --git a/samples/DockReactiveUIManagedSample/Models/DemoData.cs b/samples/DockReactiveUIManagedSample/Models/DemoData.cs new file mode 100644 index 000000000..66d562b04 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/DemoData.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models; + +public class DemoData +{ +} diff --git a/samples/DockReactiveUIManagedSample/Models/Documents/DemoDocument.cs b/samples/DockReactiveUIManagedSample/Models/Documents/DemoDocument.cs new file mode 100644 index 000000000..f20ab6df0 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/Documents/DemoDocument.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models.Documents; + +public class DemoDocument +{ +} diff --git a/samples/DockReactiveUIManagedSample/Models/Tools/Tool1.cs b/samples/DockReactiveUIManagedSample/Models/Tools/Tool1.cs new file mode 100644 index 000000000..ca5226379 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/Tools/Tool1.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models.Tools; + +public class Tool1 +{ +} diff --git a/samples/DockReactiveUIManagedSample/Models/Tools/Tool2.cs b/samples/DockReactiveUIManagedSample/Models/Tools/Tool2.cs new file mode 100644 index 000000000..7e81b9fe4 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/Tools/Tool2.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models.Tools; + +public class Tool2 +{ +} diff --git a/samples/DockReactiveUIManagedSample/Models/Tools/Tool3.cs b/samples/DockReactiveUIManagedSample/Models/Tools/Tool3.cs new file mode 100644 index 000000000..7337a9311 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/Tools/Tool3.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models.Tools; + +public class Tool3 +{ +} diff --git a/samples/DockReactiveUIManagedSample/Models/Tools/Tool4.cs b/samples/DockReactiveUIManagedSample/Models/Tools/Tool4.cs new file mode 100644 index 000000000..ab54d045e --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/Tools/Tool4.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models.Tools; + +public class Tool4 +{ +} diff --git a/samples/DockReactiveUIManagedSample/Models/Tools/Tool5.cs b/samples/DockReactiveUIManagedSample/Models/Tools/Tool5.cs new file mode 100644 index 000000000..495844c12 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/Tools/Tool5.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models.Tools; + +public class Tool5 +{ +} diff --git a/samples/DockReactiveUIManagedSample/Models/Tools/Tool6.cs b/samples/DockReactiveUIManagedSample/Models/Tools/Tool6.cs new file mode 100644 index 000000000..82cf1c3e7 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/Tools/Tool6.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models.Tools; + +public class Tool6 +{ +} diff --git a/samples/DockReactiveUIManagedSample/Models/Tools/Tool7.cs b/samples/DockReactiveUIManagedSample/Models/Tools/Tool7.cs new file mode 100644 index 000000000..f4058ca3f --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/Tools/Tool7.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models.Tools; + +public class Tool7 +{ +} diff --git a/samples/DockReactiveUIManagedSample/Models/Tools/Tool8.cs b/samples/DockReactiveUIManagedSample/Models/Tools/Tool8.cs new file mode 100644 index 000000000..4c0d7a8e5 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Models/Tools/Tool8.cs @@ -0,0 +1,6 @@ + +namespace DockReactiveUIManagedSample.Models.Tools; + +public class Tool8 +{ +} diff --git a/samples/DockReactiveUIManagedSample/Program.cs b/samples/DockReactiveUIManagedSample/Program.cs new file mode 100644 index 000000000..2bf34a21c --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Program.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. +using Avalonia; +using ReactiveUI.Avalonia; +using System; +using System.Diagnostics.CodeAnalysis; +using Dock.Settings; + +namespace DockReactiveUIManagedSample; + +[RequiresUnreferencedCode("Requires unreferenced code for App.")] +[RequiresDynamicCode("Requires unreferenced code for App.")] +internal class Program +{ + [STAThread] + private static void Main(string[] args) + { + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .UseReactiveUI() + .UseManagedWindows() + .ShowDockablePreviewOnDrag() + .SetDragPreviewOpacity(0.6) + .LogToTrace(); +} diff --git a/samples/DockReactiveUIManagedSample/Themes/FluentThemeManager.cs b/samples/DockReactiveUIManagedSample/Themes/FluentThemeManager.cs new file mode 100644 index 000000000..a6925fe95 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Themes/FluentThemeManager.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Styling; + +namespace DockReactiveUIManagedSample.Themes; + +public class FluentThemeManager : IThemeManager +{ + public void Switch(int index) + { + if (Application.Current is null) + { + return; + } + + Application.Current.RequestedThemeVariant = index switch + { + 0 => ThemeVariant.Light, + 1 => ThemeVariant.Dark, + _ => Application.Current.RequestedThemeVariant + }; + } +} diff --git a/samples/DockReactiveUIManagedSample/Themes/IThemeManager.cs b/samples/DockReactiveUIManagedSample/Themes/IThemeManager.cs new file mode 100644 index 000000000..096a413ab --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Themes/IThemeManager.cs @@ -0,0 +1,6 @@ +namespace DockReactiveUIManagedSample.Themes; + +public interface IThemeManager +{ + void Switch(int index); +} diff --git a/samples/DockReactiveUIManagedSample/ViewLocator.cs b/samples/DockReactiveUIManagedSample/ViewLocator.cs new file mode 100644 index 000000000..543bca3c2 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewLocator.cs @@ -0,0 +1,41 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +//using CommunityToolkit.Mvvm.ComponentModel; +using Dock.Model.Core; +using ReactiveUI; +using StaticViewLocator; + +namespace DockReactiveUIManagedSample; + +[StaticViewLocator] +public partial class ViewLocator : IDataTemplate +{ + public Control? Build(object? data) + { + if (data is null) + { + return null; + } + + var type = data.GetType(); + + if (s_views.TryGetValue(type, out var func)) + { + return func.Invoke(); + } + + throw new Exception($"Unable to create view for type: {type}"); + } + + public bool Match(object? data) + { + if (data is null) + { + return false; + } + + var type = data.GetType(); + return data is IDockable || s_views.ContainsKey(type); + } +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/DockFactory.cs b/samples/DockReactiveUIManagedSample/ViewModels/DockFactory.cs new file mode 100644 index 000000000..86a78ff43 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/DockFactory.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using DockReactiveUIManagedSample.Models.Documents; +using DockReactiveUIManagedSample.Models.Tools; +using DockReactiveUIManagedSample.ViewModels.Docks; +using DockReactiveUIManagedSample.ViewModels.Documents; +using DockReactiveUIManagedSample.ViewModels.Tools; +using DockReactiveUIManagedSample.ViewModels.Views; +using Dock.Avalonia.Controls; +using Dock.Settings; +using Dock.Model.Controls; +using Dock.Model.Core; +using Dock.Model.ReactiveUI; +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels; + +[RequiresUnreferencedCode("Requires unreferenced code for CustomDocumentDock.")] +[RequiresDynamicCode("Requires unreferenced code for CustomDocumentDock.")] +public class DockFactory : Factory +{ + private readonly object _context; + private IRootDock? _rootDock; + private IDocumentDock? _documentDock; + + public DockFactory(object context) + { + _context = context; + } + + public override IDocumentDock CreateDocumentDock() => new CustomDocumentDock(); + + public override IRootDock CreateLayout() + { + var document1 = new DocumentViewModel {Id = "Document1", Title = "Document1"}; + var document2 = new DocumentViewModel {Id = "Document2", Title = "Document2"}; + var document3 = new DocumentViewModel {Id = "Document3", Title = "Document3", CanClose = true}; + var tool1 = new Tool1ViewModel {Id = "Tool1", Title = "Tool1", KeepPinnedDockableVisible = true}; + var tool2 = new Tool2ViewModel {Id = "Tool2", Title = "Tool2", KeepPinnedDockableVisible = true}; + var tool3 = new Tool3ViewModel {Id = "Tool3", Title = "Tool3", CanDrag = false }; + var tool4 = new Tool4ViewModel {Id = "Tool4", Title = "Tool4", CanDrag = false }; + var tool5 = new Tool5ViewModel {Id = "Tool5", Title = "Tool5" }; + var tool6 = new Tool6ViewModel {Id = "Tool6", Title = "Tool6", CanClose = true, CanPin = true}; + var tool7 = new Tool7ViewModel {Id = "Tool7", Title = "Tool7", CanClose = false, CanPin = false}; + var tool8 = new Tool8ViewModel {Id = "Tool8", Title = "Tool8", CanClose = false, CanPin = true}; + + var leftDock = new ProportionalDock + { + Proportion = 0.25, + Orientation = Orientation.Vertical, + ActiveDockable = null, + VisibleDockables = CreateList + ( + new ToolDock + { + ActiveDockable = tool1, + VisibleDockables = CreateList(tool1, tool2), + Alignment = Alignment.Left, + // CanDrop = false + }, + new ProportionalDockSplitter { CanResize = true }, + new ToolDock + { + ActiveDockable = tool3, + VisibleDockables = CreateList(tool3, tool4), + Alignment = Alignment.Bottom, + CanDrag = false, + CanDrop = false + } + ), + // CanDrop = false + }; + + var rightDock = new ProportionalDock + { + Proportion = 0.25, + Orientation = Orientation.Vertical, + ActiveDockable = null, + VisibleDockables = CreateList + ( + new ToolDock + { + ActiveDockable = tool5, + VisibleDockables = CreateList(tool5, tool6), + Alignment = Alignment.Top, + GripMode = GripMode.Hidden + }, + new ProportionalDockSplitter(), + new ToolDock + { + ActiveDockable = tool7, + VisibleDockables = CreateList(tool7, tool8), + Alignment = Alignment.Right, + GripMode = GripMode.AutoHide + } + ), + // CanDrop = false + }; + + var documentDock = new CustomDocumentDock + { + IsCollapsable = false, + ActiveDockable = document1, + VisibleDockables = CreateList(document1, document2, document3), + CanCreateDocument = true, + // CanDrop = false, + EnableWindowDrag = true, + // CanCloseLastDockable = false, + }; + + var mainLayout = new ProportionalDock + { + Orientation = Orientation.Horizontal, + VisibleDockables = CreateList + ( + leftDock, + new ProportionalDockSplitter(), + documentDock, + new ProportionalDockSplitter(), + rightDock + ) + }; + + var dashboardView = new DashboardViewModel + { + Id = "Dashboard", + Title = "Dashboard" + }; + + var homeView = new HomeViewModel + { + Id = "Home", + Title = "Home", + ActiveDockable = mainLayout, + VisibleDockables = CreateList(mainLayout) + }; + + var rootDock = CreateRootDock(); + + rootDock.IsCollapsable = false; + rootDock.ActiveDockable = dashboardView; + rootDock.DefaultDockable = homeView; + rootDock.VisibleDockables = CreateList(dashboardView, homeView); + + rootDock.LeftPinnedDockables = CreateList(); + rootDock.RightPinnedDockables = CreateList(); + rootDock.TopPinnedDockables = CreateList(); + rootDock.BottomPinnedDockables = CreateList(); + + rootDock.PinnedDock = null; + + _documentDock = documentDock; + _rootDock = rootDock; + + return rootDock; + } + + public override IDockWindow? CreateWindowFrom(IDockable dockable) + { + var window = base.CreateWindowFrom(dockable); + + if (window != null) + { + window.Title = "Dock Avalonia Demo"; + } + return window; + } + + public override void InitLayout(IDockable layout) + { + ContextLocator = new Dictionary> + { + ["Document1"] = () => new DemoDocument(), + ["Document2"] = () => new DemoDocument(), + ["Document3"] = () => new DemoDocument(), + ["Tool1"] = () => new Tool1(), + ["Tool2"] = () => new Tool2(), + ["Tool3"] = () => new Tool3(), + ["Tool4"] = () => new Tool4(), + ["Tool5"] = () => new Tool5(), + ["Tool6"] = () => new Tool6(), + ["Tool7"] = () => new Tool7(), + ["Tool8"] = () => new Tool8(), + ["Dashboard"] = () => layout, + ["Home"] = () => _context + }; + + DockableLocator = new Dictionary>() + { + ["Root"] = () => _rootDock, + ["Documents"] = () => _documentDock + }; + + HostWindowLocator = new Dictionary> + { + [nameof(IDockWindow)] = () => DockSettings.UseManagedWindows ? new ManagedHostWindow() : new HostWindow() + }; + + base.InitLayout(layout); + } +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Docks/CustomDocumentDock.cs b/samples/DockReactiveUIManagedSample/ViewModels/Docks/CustomDocumentDock.cs new file mode 100644 index 000000000..c529738cc --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Docks/CustomDocumentDock.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using DockReactiveUIManagedSample.ViewModels.Documents; +using Dock.Model.ReactiveUI.Controls; +using ReactiveUI; + +namespace DockReactiveUIManagedSample.ViewModels.Docks; + +[RequiresUnreferencedCode("Requires unreferenced code for ReactiveCommand.Create.")] +[RequiresDynamicCode("Requires unreferenced code for ReactiveCommand.Create.")] +public class CustomDocumentDock : DocumentDock +{ + public CustomDocumentDock() + { + CreateDocument = ReactiveCommand.Create(CreateNewDocument); + } + + private void CreateNewDocument() + { + if (!CanCreateDocument) + { + return; + } + + var index = VisibleDockables?.Count + 1; + var document = new DocumentViewModel {Id = $"Document{index}", Title = $"Document{index}"}; + + Factory?.AddDockable(this, document); + Factory?.SetActiveDockable(document); + Factory?.SetFocusedDockable(this, document); + } +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Documents/DocumentViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Documents/DocumentViewModel.cs new file mode 100644 index 000000000..3b74b4afc --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Documents/DocumentViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Documents; + +public class DocumentViewModel : Document +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/MainWindowViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/MainWindowViewModel.cs new file mode 100644 index 000000000..cd9c74987 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,197 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Input; +using DockReactiveUIManagedSample.Models; +using Dock.Model.Controls; +using Dock.Model.Core; +using ReactiveUI; + +namespace DockReactiveUIManagedSample.ViewModels; + +[RequiresUnreferencedCode("Requires unreferenced code for RaiseAndSetIfChanged.")] +[RequiresDynamicCode("Requires unreferenced code for RaiseAndSetIfChanged.")] +public class MainWindowViewModel : ReactiveObject +{ + private readonly IFactory? _factory; + private IRootDock? _layout; + + public IRootDock? Layout + { + get => _layout; + set => this.RaiseAndSetIfChanged(ref _layout, value); + } + + public ICommand NewLayout { get; } + + public MainWindowViewModel() + { + _factory = new DockFactory(new DemoData()); + + DebugFactoryEvents(_factory); + + var layout = _factory?.CreateLayout(); + if (layout is not null) + { + _factory?.InitLayout(layout); + layout.Navigate.Execute("Home"); + } + Layout = layout; + + NewLayout = ReactiveCommand.Create(ResetLayout); + } + + private void DebugFactoryEvents(IFactory factory) + { + factory.ActiveDockableChanged += (_, args) => + { + Debug.WriteLine($"[ActiveDockableChanged] Title='{args.Dockable?.Title}'"); + }; + + factory.FocusedDockableChanged += (_, args) => + { + Debug.WriteLine($"[FocusedDockableChanged] Title='{args.Dockable?.Title}'"); + }; + + factory.DockableAdded += (_, args) => + { + Debug.WriteLine($"[DockableAdded] Title='{args.Dockable?.Title}'"); + }; + + factory.DockableRemoved += (_, args) => + { + Debug.WriteLine($"[DockableRemoved] Title='{args.Dockable?.Title}'"); + }; + + factory.DockableClosed += (_, args) => + { + Debug.WriteLine($"[DockableClosed] Title='{args.Dockable?.Title}'"); + }; + + factory.DockableMoved += (_, args) => + { + Debug.WriteLine($"[DockableMoved] Title='{args.Dockable?.Title}'"); + }; + + factory.DockableDocked += (_, args) => + { + Debug.WriteLine($"[DockableDocked] Title='{args.Dockable?.Title}', Operation='{args.Operation}'"); + }; + + factory.DockableUndocked += (_, args) => + { + Debug.WriteLine($"[DockableUndocked] Title='{args.Dockable?.Title}', Operation='{args.Operation}'"); + }; + + factory.DockableSwapped += (_, args) => + { + Debug.WriteLine($"[DockableSwapped] Title='{args.Dockable?.Title}'"); + }; + + factory.DockablePinned += (_, args) => + { + Debug.WriteLine($"[DockablePinned] Title='{args.Dockable?.Title}'"); + }; + + factory.DockableUnpinned += (_, args) => + { + Debug.WriteLine($"[DockableUnpinned] Title='{args.Dockable?.Title}'"); + }; + + factory.WindowOpened += (_, args) => + { + Debug.WriteLine($"[WindowOpened] Title='{args.Window?.Title}'"); + }; + + factory.WindowClosed += (_, args) => + { + Debug.WriteLine($"[WindowClosed] Title='{args.Window?.Title}'"); + }; + + factory.WindowClosing += (_, args) => + { + // NOTE: Set to True to cancel window closing. +#if false + args.Cancel = true; +#endif + Debug.WriteLine($"[WindowClosing] Title='{args.Window?.Title}', Cancel={args.Cancel}"); + }; + + factory.WindowAdded += (_, args) => + { + Debug.WriteLine($"[WindowAdded] Title='{args.Window?.Title}'"); + }; + + factory.WindowRemoved += (_, args) => + { + Debug.WriteLine($"[WindowRemoved] Title='{args.Window?.Title}'"); + }; + + factory.WindowMoveDragBegin += (_, args) => + { + // NOTE: Set to True to cancel window dragging. +#if false + args.Cancel = true; +#endif + Debug.WriteLine($"[WindowMoveDragBegin] Title='{args.Window?.Title}', Cancel={args.Cancel}, X='{args.Window?.X}', Y='{args.Window?.Y}'"); + }; + + factory.WindowMoveDrag += (_, args) => + { + Debug.WriteLine($"[WindowMoveDrag] Title='{args.Window?.Title}', X='{args.Window?.X}', Y='{args.Window?.Y}"); + }; + + factory.WindowMoveDragEnd += (_, args) => + { + Debug.WriteLine($"[WindowMoveDragEnd] Title='{args.Window?.Title}', X='{args.Window?.X}', Y='{args.Window?.Y}"); + }; + + factory.WindowActivated += (_, args) => + { + Debug.WriteLine($"[WindowActivated] Title='{args.Window?.Title}'"); + }; + + factory.DockableActivated += (_, args) => + { + Debug.WriteLine($"[DockableActivated] Title='{args.Dockable?.Title}'"); + }; + + factory.WindowDeactivated += (_, args) => + { + Debug.WriteLine($"[WindowDeactivated] Title='{args.Window?.Title}'"); + }; + + factory.DockableDeactivated += (_, args) => + { + Debug.WriteLine($"[DockableDeactivated] Title='{args.Dockable?.Title}'"); + }; + } + + public void CloseLayout() + { + if (Layout is IDock dock) + { + if (dock.Close.CanExecute(null)) + { + dock.Close.Execute(null); + } + } + } + + public void ResetLayout() + { + if (Layout is not null) + { + if (Layout.Close.CanExecute(null)) + { + Layout.Close.Execute(null); + } + } + + var layout = _factory?.CreateLayout(); + if (layout is not null) + { + _factory?.InitLayout(layout); + Layout = layout; + } + } +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool1ViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool1ViewModel.cs new file mode 100644 index 000000000..b86b1c2d6 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool1ViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Tools; + +public class Tool1ViewModel : Tool +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool2ViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool2ViewModel.cs new file mode 100644 index 000000000..9a39a8a86 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool2ViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Tools; + +public class Tool2ViewModel : Tool +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool3ViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool3ViewModel.cs new file mode 100644 index 000000000..759a7e557 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool3ViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Tools; + +public class Tool3ViewModel : Tool +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool4ViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool4ViewModel.cs new file mode 100644 index 000000000..5a1ba4152 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool4ViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Tools; + +public class Tool4ViewModel : Tool +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool5ViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool5ViewModel.cs new file mode 100644 index 000000000..1d5c7d394 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool5ViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Tools; + +public class Tool5ViewModel : Tool +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool6ViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool6ViewModel.cs new file mode 100644 index 000000000..dead083a5 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool6ViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Tools; + +public class Tool6ViewModel : Tool +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool7ViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool7ViewModel.cs new file mode 100644 index 000000000..e57d75680 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool7ViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Tools; + +public class Tool7ViewModel : Tool +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool8ViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool8ViewModel.cs new file mode 100644 index 000000000..af4765c90 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Tools/Tool8ViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Tools; + +public class Tool8ViewModel : Tool +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Views/DashboardViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Views/DashboardViewModel.cs new file mode 100644 index 000000000..6fb209f1f --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Views/DashboardViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Core; + +namespace DockReactiveUIManagedSample.ViewModels.Views; + +public class DashboardViewModel : DockBase +{ +} diff --git a/samples/DockReactiveUIManagedSample/ViewModels/Views/HomeViewModel.cs b/samples/DockReactiveUIManagedSample/ViewModels/Views/HomeViewModel.cs new file mode 100644 index 000000000..be4263db6 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/ViewModels/Views/HomeViewModel.cs @@ -0,0 +1,7 @@ +using Dock.Model.ReactiveUI.Controls; + +namespace DockReactiveUIManagedSample.ViewModels.Views; + +public class HomeViewModel : RootDock +{ +} diff --git a/samples/DockReactiveUIManagedSample/Views/DockableOptionsView.axaml b/samples/DockReactiveUIManagedSample/Views/DockableOptionsView.axaml new file mode 100644 index 000000000..d8ebf6812 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Views/DockableOptionsView.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/samples/DockReactiveUIManagedSample/Views/DockableOptionsView.axaml.cs b/samples/DockReactiveUIManagedSample/Views/DockableOptionsView.axaml.cs new file mode 100644 index 000000000..28b6508d0 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Views/DockableOptionsView.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace DockReactiveUIManagedSample.Views; + +public partial class DockableOptionsView : UserControl +{ + public DockableOptionsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/samples/DockReactiveUIManagedSample/Views/Documents/DocumentView.axaml b/samples/DockReactiveUIManagedSample/Views/Documents/DocumentView.axaml new file mode 100644 index 000000000..41ede0d87 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Views/Documents/DocumentView.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/samples/DockReactiveUIManagedSample/Views/Documents/DocumentView.axaml.cs b/samples/DockReactiveUIManagedSample/Views/Documents/DocumentView.axaml.cs new file mode 100644 index 000000000..ee5fe4153 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Views/Documents/DocumentView.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace DockReactiveUIManagedSample.Views.Documents; + +public partial class DocumentView : UserControl +{ + public DocumentView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/samples/DockReactiveUIManagedSample/Views/MainView.axaml b/samples/DockReactiveUIManagedSample/Views/MainView.axaml new file mode 100644 index 000000000..1e0718b58 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Views/MainView.axaml @@ -0,0 +1,75 @@ + + + + + + M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Zm0-2V4a8 8 0 1 1 0 16Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/DockReactiveUIManagedSample/Views/MainView.axaml.cs b/samples/DockReactiveUIManagedSample/Views/MainView.axaml.cs new file mode 100644 index 000000000..3ba6010f1 --- /dev/null +++ b/samples/DockReactiveUIManagedSample/Views/MainView.axaml.cs @@ -0,0 +1,35 @@ +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace DockReactiveUIManagedSample.Views; + +[RequiresUnreferencedCode("Requires unreferenced code for ThemeManager.")] +[RequiresDynamicCode("Requires unreferenced code for ThemeManager.")] +public partial class MainView : UserControl +{ + public MainView() + { + InitializeComponent(); + InitializeThemes(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void InitializeThemes() + { + var dark = false; + var theme = this.Find + + + + + + + diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/WindowChromeResources.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/WindowChromeResources.axaml new file mode 100644 index 000000000..9a568a549 --- /dev/null +++ b/src/Dock.Avalonia.Themes.Fluent/Controls/WindowChromeResources.axaml @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml b/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml index 80d2a1d87..6154426ff 100644 --- a/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml +++ b/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml @@ -1,11 +1,14 @@  + + @@ -36,6 +39,7 @@ + @@ -50,8 +54,19 @@ - + + diff --git a/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml b/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml index 210d332db..62024acec 100644 --- a/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml +++ b/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml @@ -1,11 +1,14 @@  + + @@ -36,14 +39,26 @@ + - + + diff --git a/src/Dock.Avalonia/Controls/DockControl.axaml.cs b/src/Dock.Avalonia/Controls/DockControl.axaml.cs index a52fd2e7c..91122113f 100644 --- a/src/Dock.Avalonia/Controls/DockControl.axaml.cs +++ b/src/Dock.Avalonia/Controls/DockControl.axaml.cs @@ -5,6 +5,7 @@ using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Recycling; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -32,12 +33,14 @@ namespace Dock.Avalonia.Controls; [TemplatePart("PART_ContentControl", typeof(ContentControl))] [TemplatePart("PART_CommandBarHost", typeof(DockCommandBarHost))] [TemplatePart("PART_SelectorOverlay", typeof(DockSelectorOverlay))] +[TemplatePart("PART_ManagedWindowLayer", typeof(ManagedWindowLayer))] public class DockControl : TemplatedControl, IDockControl, IDockSelectorService { private readonly DockManager _dockManager; private readonly DockControlState _dockControlState; private bool _isInitialized; private ContentControl? _contentControl; + private ManagedWindowLayer? _managedWindowLayer; private DockCommandBarHost? _commandBarHost; private DockCommandBarManager? _commandBarManager; private DockSelectorOverlay? _selectorOverlay; @@ -46,6 +49,7 @@ public class DockControl : TemplatedControl, IDockControl, IDockSelectorService private readonly Dictionary _activationOrder = new(); private long _activationCounter; private IFactory? _subscribedFactory; + private IFactory? _managedLayerFactory; /// /// Defines the property. @@ -71,6 +75,12 @@ public class DockControl : TemplatedControl, IDockControl, IDockSelectorService public static readonly StyledProperty InitializeFactoryProperty = AvaloniaProperty.Register(nameof(InitializeFactory)); + /// + /// Defines the property. + /// + public static readonly StyledProperty?> HostWindowFactoryProperty = + AvaloniaProperty.Register?>(nameof(HostWindowFactory)); + /// /// Defines the property. /// @@ -83,6 +93,12 @@ public class DockControl : TemplatedControl, IDockControl, IDockSelectorService public static readonly StyledProperty IsDraggingDockProperty = AvaloniaProperty.Register(nameof(IsDraggingDock)); + /// + /// Defines the property. + /// + public static readonly StyledProperty EnableManagedWindowLayerProperty = + AvaloniaProperty.Register(nameof(EnableManagedWindowLayer), true); + /// /// Defines the property. /// @@ -124,6 +140,15 @@ public bool InitializeFactory set => SetValue(InitializeFactoryProperty, value); } + /// + /// Gets or sets the factory used to create host windows. + /// + public Func? HostWindowFactory + { + get => GetValue(HostWindowFactoryProperty); + set => SetValue(HostWindowFactoryProperty, value); + } + /// public IFactory? Factory { @@ -140,6 +165,15 @@ public bool IsDraggingDock set => SetValue(IsDraggingDockProperty, value); } + /// + /// Gets or sets whether the managed window layer is enabled for this control. + /// + public bool EnableManagedWindowLayer + { + get => GetValue(EnableManagedWindowLayerProperty); + set => SetValue(EnableManagedWindowLayerProperty, value); + } + /// /// Gets or sets whether to automatically create default DataTemplates in code-behind. /// When true (default), the control will add default DataTemplates for all dock types. @@ -198,15 +232,33 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _contentControl = e.NameScope.Find("PART_ContentControl"); _commandBarHost = e.NameScope.Find("PART_CommandBarHost"); _selectorOverlay = e.NameScope.Find("PART_SelectorOverlay"); + _managedWindowLayer = e.NameScope.Find("PART_ManagedWindowLayer"); + + InitializeControlRecycling(); if (_contentControl is not null) { InitializeDefaultDataTemplates(); } + UpdateManagedWindowLayer(Layout); InitializeCommandBars(); } + private void InitializeControlRecycling() + { + var recycling = ControlRecyclingDataTemplate.GetControlRecycling(this); + if (recycling is ControlRecycling shared) + { + var local = new ControlRecycling + { + TryToUseIdAsKey = shared.TryToUseIdAsKey + }; + + ControlRecyclingDataTemplate.SetControlRecycling(this, local); + } + } + private void InitializeDefaultDataTemplates() { if (_contentControl?.DataTemplates is null) @@ -241,6 +293,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang Initialize(change.GetNewValue()); } + else if (change.Property == EnableManagedWindowLayerProperty) + { + UpdateManagedWindowLayer(Layout); + } } private void Initialize(IDock? layout) @@ -264,18 +320,34 @@ private void Initialize(IDock? layout) layout.Factory.DockControls.Add(this); + UpdateManagedWindowLayer(layout); + if (InitializeFactory) { - layout.Factory.ContextLocator = new Dictionary>(); - layout.Factory.HostWindowLocator = new Dictionary> + layout.Factory.ContextLocator ??= new Dictionary>(); + layout.Factory.DockableLocator ??= new Dictionary>(); + layout.Factory.DefaultContextLocator ??= GetContext; + layout.Factory.DefaultHostWindowLocator ??= GetHostWindow; + + if (layout.Factory.HostWindowLocator is null) { - [nameof(IDockWindow)] = () => new HostWindow() - }; - layout.Factory.DockableLocator = new Dictionary>(); - layout.Factory.DefaultContextLocator = GetContext; - layout.Factory.DefaultHostWindowLocator = GetHostWindow; + layout.Factory.HostWindowLocator = new Dictionary> + { + [nameof(IDockWindow)] = GetHostWindow + }; + } + + IHostWindow GetHostWindow() + { + if (HostWindowFactory is { } factory) + { + return factory(); + } - IHostWindow GetHostWindow() => new HostWindow(); + return DockSettings.UseManagedWindows + ? new ManagedHostWindow() + : new HostWindow(); + } object? GetContext() => DefaultContext; } @@ -297,6 +369,8 @@ private void Initialize(IDock? layout) private void DeInitialize(IDock? layout) { + UnregisterManagedWindowLayer(); + if (layout?.Factory is null) { return; @@ -381,6 +455,41 @@ private void FactoryActiveDockableChanged(object? sender, ActiveDockableChangedE _activationOrder[e.Dockable] = ++_activationCounter; } + private void UpdateManagedWindowLayer(IDock? layout) + { + if (_managedWindowLayer is null) + { + return; + } + + if (EnableManagedWindowLayer && DockSettings.UseManagedWindows && layout?.Factory is { } factory) + { + if (!ReferenceEquals(_managedLayerFactory, factory)) + { + UnregisterManagedWindowLayer(); + } + + ManagedWindowRegistry.RegisterLayer(factory, _managedWindowLayer); + _managedLayerFactory = factory; + _managedWindowLayer.IsVisible = true; + return; + } + + UnregisterManagedWindowLayer(); + _managedWindowLayer.IsVisible = false; + } + + private void UnregisterManagedWindowLayer() + { + if (_managedWindowLayer is null || _managedLayerFactory is null) + { + return; + } + + ManagedWindowRegistry.UnregisterLayer(_managedLayerFactory, _managedWindowLayer); + _managedLayerFactory = null; + } + /// public void ShowSelector(DockSelectorMode mode) { diff --git a/src/Dock.Avalonia/Controls/DockableControl.cs b/src/Dock.Avalonia/Controls/DockableControl.cs index 5eb78105c..23dc5fe0b 100644 --- a/src/Dock.Avalonia/Controls/DockableControl.cs +++ b/src/Dock.Avalonia/Controls/DockableControl.cs @@ -9,6 +9,7 @@ using Avalonia.VisualTree; using Dock.Avalonia.Internal; using Dock.Model.Core; +using Dock.Settings; namespace Dock.Avalonia.Controls; @@ -207,9 +208,20 @@ private void SetPointerTracking(PointerEventArgs e) } var screenPoint = DockHelpers.GetScreenPoint(this, position); - var screenPosition = DockHelpers.ToDockPoint(screenPoint); + var pointerX = screenPoint.X; + var pointerY = screenPoint.Y; + + if (DockSettings.UseManagedWindows && ManagedWindowLayer.TryGetLayer(this) is { } layer) + { + var layerPoint = this.TranslatePoint(position, layer); + if (layerPoint.HasValue) + { + pointerX = layerPoint.Value.X; + pointerY = layerPoint.Value.Y; + } + } dockable.SetPointerPosition(position.X, position.Y); - dockable.SetPointerScreenPosition(screenPosition.X, screenPosition.Y); + dockable.SetPointerScreenPosition(pointerX, pointerY); } } diff --git a/src/Dock.Avalonia/Controls/DragPreviewControl.axaml.cs b/src/Dock.Avalonia/Controls/DragPreviewControl.axaml.cs index d34525ae0..2cb18f8ad 100644 --- a/src/Dock.Avalonia/Controls/DragPreviewControl.axaml.cs +++ b/src/Dock.Avalonia/Controls/DragPreviewControl.axaml.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Recycling; +using Avalonia.Controls.Recycling.Model; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -42,6 +44,26 @@ public class DragPreviewControl : TemplatedControl /// public static readonly StyledProperty PreviewContentHeightProperty = AvaloniaProperty.Register(nameof(PreviewContentHeight), double.NaN); + + /// + /// Defines property. + /// + public static readonly StyledProperty ControlRecyclingProperty = + AvaloniaProperty.Register(nameof(ControlRecycling)); + + /// + /// Defines property. + /// + public static readonly StyledProperty PreviewContentProperty = + AvaloniaProperty.Register(nameof(PreviewContent)); + + /// + /// Initializes a new instance of the class. + /// + public DragPreviewControl() + { + ControlRecycling = new ControlRecycling(); + } /// /// Gets or sets tab header template. @@ -87,4 +109,22 @@ public double PreviewContentHeight get => GetValue(PreviewContentHeightProperty); set => SetValue(PreviewContentHeightProperty, value); } + + /// + /// Gets or sets the control recycling instance for preview content. + /// + public IControlRecycling? ControlRecycling + { + get => GetValue(ControlRecyclingProperty); + set => SetValue(ControlRecyclingProperty, value); + } + + /// + /// Gets or sets the managed preview content. + /// + public Control? PreviewContent + { + get => GetValue(PreviewContentProperty); + set => SetValue(PreviewContentProperty, value); + } } diff --git a/src/Dock.Avalonia/Controls/ManagedDockWindowDocument.cs b/src/Dock.Avalonia/Controls/ManagedDockWindowDocument.cs new file mode 100644 index 000000000..eb15990c5 --- /dev/null +++ b/src/Dock.Avalonia/Controls/ManagedDockWindowDocument.cs @@ -0,0 +1,361 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Dock.Avalonia.Internal; +using Dock.Model.Controls; +using Dock.Model.Core; + +namespace Dock.Avalonia.Controls; + +/// +/// Document model used for managed floating windows. +/// +public sealed class ManagedDockWindowDocument : ManagedDockableBase, IMdiDocument, IDocumentContent, IRecyclingDataTemplate, IDisposable +{ + private DockRect _mdiBounds; + private MdiWindowState _mdiState = MdiWindowState.Normal; + private int _mdiZIndex; + private object? _content; + private Type? _dataType; + private IDockWindow? _window; + private INotifyPropertyChanged? _windowSubscription; + private INotifyPropertyChanged? _layoutSubscription; + private INotifyPropertyChanged? _focusedDockableSubscription; + private IDockable? _focusedDockable; + + /// + /// Initializes a new instance of the class. + /// + public ManagedDockWindowDocument(IDockWindow window) + { + _window = window; + Id = window.Id; + Title = window.Title; + Context = window; + AttachWindow(window); + AttachLayout(window.Layout); + UpdateTitleFromLayout(); + } + + /// + /// Gets the associated dock window. + /// + public IDockWindow? Window => _window; + + /// + /// Gets whether the managed window hosts a tool dock. + /// + public bool IsToolWindow => _window?.Layout?.ActiveDockable is IToolDock; + + /// + /// Gets the tool dock hosted by this managed window, if any. + /// + public IToolDock? ToolDock => _window?.Layout?.ActiveDockable as IToolDock; + + /// + /// Gets or sets the content to display. + /// + public object? Content + { + get => _content; + set => SetProperty(ref _content, value); + } + + /// + /// Gets or sets the data type for template matching. + /// + public Type? DataType + { + get => _dataType; + set => SetProperty(ref _dataType, value); + } + + public DockRect MdiBounds + { + get => _mdiBounds; + set + { + if (SetProperty(ref _mdiBounds, value)) + { + SyncBounds(); + } + } + } + + public MdiWindowState MdiState + { + get => _mdiState; + set => SetProperty(ref _mdiState, value); + } + + public int MdiZIndex + { + get => _mdiZIndex; + set => SetProperty(ref _mdiZIndex, value); + } + + public override bool OnClose() + { + if (_window is null) + { + return true; + } + + if (_window.Host is ManagedHostWindow managedHost) + { + managedHost.Exit(); + return !managedHost.LastCloseCanceled; + } + + _window.Exit(); + return true; + } + + public bool Match(object? data) + { + if (DataType is null) + { + return true; + } + + return DataType.IsInstanceOfType(data); + } + + public Control? Build(object? data) => Build(data, null); + + public Control? Build(object? data, Control? existing) + { + return BuildContent(Content, this); + } + + public void Dispose() + { + DetachFocusedDockable(); + DetachLayout(); + DetachWindow(); + _window = null; + Content = null; + } + + protected override void OnPropertyChanged(string propertyName) + { + base.OnPropertyChanged(propertyName); + + if (_window is null) + { + return; + } + + if (propertyName == nameof(Title)) + { + _window.Title = Title; + } + else if (propertyName == nameof(Id)) + { + _window.Id = Id; + } + } + + private void AttachWindow(IDockWindow? window) + { + DetachWindow(); + + if (window is not INotifyPropertyChanged windowChanged) + { + return; + } + + _windowSubscription = windowChanged; + _windowSubscription.PropertyChanged += WindowPropertyChanged; + } + + private void DetachWindow() + { + if (_windowSubscription is null) + { + return; + } + + _windowSubscription.PropertyChanged -= WindowPropertyChanged; + _windowSubscription = null; + } + + private void WindowPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IDockWindow.Layout)) + { + AttachLayout(_window?.Layout); + UpdateTitleFromLayout(); + return; + } + + if (e.PropertyName == nameof(IDockWindow.Title)) + { + if (_window?.Layout?.FocusedDockable is null) + { + UpdateTitleFromLayout(); + } + } + } + + private void AttachLayout(IDock? layout) + { + DetachLayout(); + + if (layout is not INotifyPropertyChanged layoutChanged) + { + return; + } + + _layoutSubscription = layoutChanged; + _layoutSubscription.PropertyChanged += LayoutPropertyChanged; + } + + private void DetachLayout() + { + if (_layoutSubscription is null) + { + return; + } + + _layoutSubscription.PropertyChanged -= LayoutPropertyChanged; + _layoutSubscription = null; + } + + private void LayoutPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == nameof(IDock.FocusedDockable)) + { + UpdateTitleFromLayout(); + } + } + + private void UpdateTitleFromLayout() + { + var focusedDockable = _window?.Layout?.FocusedDockable; + UpdateFocusedDockableSubscription(focusedDockable); + + var focusedTitle = focusedDockable?.Title; + if (!string.IsNullOrEmpty(focusedTitle)) + { + Title = focusedTitle; + return; + } + + if (_window is { } window) + { + Title = window.Title; + } + } + + private void UpdateFocusedDockableSubscription(IDockable? focusedDockable) + { + if (ReferenceEquals(_focusedDockable, focusedDockable)) + { + return; + } + + DetachFocusedDockable(); + + _focusedDockable = focusedDockable; + if (_focusedDockable is INotifyPropertyChanged focusedChanged) + { + _focusedDockableSubscription = focusedChanged; + _focusedDockableSubscription.PropertyChanged += FocusedDockablePropertyChanged; + } + } + + private void DetachFocusedDockable() + { + if (_focusedDockableSubscription is null) + { + _focusedDockable = null; + return; + } + + _focusedDockableSubscription.PropertyChanged -= FocusedDockablePropertyChanged; + _focusedDockableSubscription = null; + _focusedDockable = null; + } + + private void FocusedDockablePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == nameof(IDockable.Title)) + { + UpdateTitleFromLayout(); + } + } + + private void SyncBounds() + { + if (_window is null) + { + return; + } + + _window.X = _mdiBounds.X; + _window.Y = _mdiBounds.Y; + _window.Width = _mdiBounds.Width; + _window.Height = _mdiBounds.Height; + + if (_window.Host is ManagedHostWindow managedHost) + { + managedHost.UpdateBoundsFromDocument(_mdiBounds); + return; + } + + if (_window.Host is IHostWindow host) + { + host.SetPosition(_mdiBounds.X, _mdiBounds.Y); + host.SetSize(_mdiBounds.Width, _mdiBounds.Height); + } + } + + private static Control? BuildContent(object? content, IDockable dockable) + { + if (DragPreviewContext.IsPreviewing(dockable)) + { + return BuildPreviewContent(content); + } + + if (content is null) + { + return null; + } + + if (content is Control directControl) + { + return directControl; + } + + if (content is Func direct) + { + return direct(null!) as Control; + } + + return TemplateContent.Load(content)?.Result; + } + + private static Control BuildPreviewContent(object? content) + { + if (content is not Control visualControl) + { + return new Panel(); + } + + return new Border + { + Background = new VisualBrush + { + Visual = visualControl, + Stretch = Stretch.Uniform + }, + ClipToBounds = true + }; + } +} diff --git a/src/Dock.Avalonia/Controls/ManagedDockableBase.cs b/src/Dock.Avalonia/Controls/ManagedDockableBase.cs new file mode 100644 index 000000000..c2c8774f5 --- /dev/null +++ b/src/Dock.Avalonia/Controls/ManagedDockableBase.cs @@ -0,0 +1,336 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Dock.Model.Adapters; +using Dock.Model.Core; + +namespace Dock.Avalonia.Controls; + +public abstract class ManagedDockableBase : IDockable, IDockSelectorInfo, INotifyPropertyChanged +{ + private readonly TrackingAdapter _trackingAdapter = new(); + private string _id = string.Empty; + private string _title = string.Empty; + private object? _context; + private IDockable? _owner; + private IDockable? _originalOwner; + private IFactory? _factory; + private bool _isEmpty; + private bool _isCollapsable = true; + private double _proportion = double.NaN; + private DockMode _dock = DockMode.Center; + private int _column; + private int _row; + private int _columnSpan = 1; + private int _rowSpan = 1; + private bool _isSharedSizeScope; + private double _collapsedProportion = double.NaN; + private double _minWidth = double.NaN; + private double _maxWidth = double.NaN; + private double _minHeight = double.NaN; + private double _maxHeight = double.NaN; + private bool _canClose = true; + private bool _canPin = true; + private bool _keepPinnedDockableVisible; + private bool _canFloat = true; + private bool _canDrag = true; + private bool _canDrop = true; + private bool _canDockAsDocument = true; + private bool _isModified; + private string? _dockGroup; + private bool _showInSelector = true; + private string? _selectorTitle; + + public event PropertyChangedEventHandler? PropertyChanged; + + public string Id + { + get => _id; + set => SetProperty(ref _id, value); + } + + public string Title + { + get => _title; + set => SetProperty(ref _title, value); + } + + public object? Context + { + get => _context; + set => SetProperty(ref _context, value); + } + + public IDockable? Owner + { + get => _owner; + set => SetProperty(ref _owner, value); + } + + public IDockable? OriginalOwner + { + get => _originalOwner; + set => SetProperty(ref _originalOwner, value); + } + + public IFactory? Factory + { + get => _factory; + set => SetProperty(ref _factory, value); + } + + public bool IsEmpty + { + get => _isEmpty; + set => SetProperty(ref _isEmpty, value); + } + + public bool IsCollapsable + { + get => _isCollapsable; + set => SetProperty(ref _isCollapsable, value); + } + + public double Proportion + { + get => _proportion; + set => SetProperty(ref _proportion, value); + } + + public DockMode Dock + { + get => _dock; + set => SetProperty(ref _dock, value); + } + + public int Column + { + get => _column; + set => SetProperty(ref _column, value); + } + + public int Row + { + get => _row; + set => SetProperty(ref _row, value); + } + + public int ColumnSpan + { + get => _columnSpan; + set => SetProperty(ref _columnSpan, value); + } + + public int RowSpan + { + get => _rowSpan; + set => SetProperty(ref _rowSpan, value); + } + + public bool IsSharedSizeScope + { + get => _isSharedSizeScope; + set => SetProperty(ref _isSharedSizeScope, value); + } + + public double CollapsedProportion + { + get => _collapsedProportion; + set => SetProperty(ref _collapsedProportion, value); + } + + public double MinWidth + { + get => _minWidth; + set => SetProperty(ref _minWidth, value); + } + + public double MaxWidth + { + get => _maxWidth; + set => SetProperty(ref _maxWidth, value); + } + + public double MinHeight + { + get => _minHeight; + set => SetProperty(ref _minHeight, value); + } + + public double MaxHeight + { + get => _maxHeight; + set => SetProperty(ref _maxHeight, value); + } + + public bool CanClose + { + get => _canClose; + set => SetProperty(ref _canClose, value); + } + + public bool CanPin + { + get => _canPin; + set => SetProperty(ref _canPin, value); + } + + public bool KeepPinnedDockableVisible + { + get => _keepPinnedDockableVisible; + set => SetProperty(ref _keepPinnedDockableVisible, value); + } + + public bool CanFloat + { + get => _canFloat; + set => SetProperty(ref _canFloat, value); + } + + public bool CanDrag + { + get => _canDrag; + set => SetProperty(ref _canDrag, value); + } + + public bool CanDrop + { + get => _canDrop; + set => SetProperty(ref _canDrop, value); + } + + public bool CanDockAsDocument + { + get => _canDockAsDocument; + set => SetProperty(ref _canDockAsDocument, value); + } + + public bool IsModified + { + get => _isModified; + set => SetProperty(ref _isModified, value); + } + + public string? DockGroup + { + get => _dockGroup; + set => SetProperty(ref _dockGroup, value); + } + + public bool ShowInSelector + { + get => _showInSelector; + set => SetProperty(ref _showInSelector, value); + } + + public string? SelectorTitle + { + get => _selectorTitle; + set => SetProperty(ref _selectorTitle, value); + } + + public string? GetControlRecyclingId() => _id; + + public virtual bool OnClose() + { + return true; + } + + public virtual void OnSelected() + { + } + + public void GetVisibleBounds(out double x, out double y, out double width, out double height) + { + _trackingAdapter.GetVisibleBounds(out x, out y, out width, out height); + } + + public void SetVisibleBounds(double x, double y, double width, double height) + { + _trackingAdapter.SetVisibleBounds(x, y, width, height); + OnVisibleBoundsChanged(x, y, width, height); + } + + public virtual void OnVisibleBoundsChanged(double x, double y, double width, double height) + { + } + + public void GetPinnedBounds(out double x, out double y, out double width, out double height) + { + _trackingAdapter.GetPinnedBounds(out x, out y, out width, out height); + } + + public void SetPinnedBounds(double x, double y, double width, double height) + { + _trackingAdapter.SetPinnedBounds(x, y, width, height); + OnPinnedBoundsChanged(x, y, width, height); + } + + public virtual void OnPinnedBoundsChanged(double x, double y, double width, double height) + { + } + + public void GetTabBounds(out double x, out double y, out double width, out double height) + { + _trackingAdapter.GetTabBounds(out x, out y, out width, out height); + } + + public void SetTabBounds(double x, double y, double width, double height) + { + _trackingAdapter.SetTabBounds(x, y, width, height); + OnTabBoundsChanged(x, y, width, height); + } + + public virtual void OnTabBoundsChanged(double x, double y, double width, double height) + { + } + + public void GetPointerPosition(out double x, out double y) + { + _trackingAdapter.GetPointerPosition(out x, out y); + } + + public void SetPointerPosition(double x, double y) + { + _trackingAdapter.SetPointerPosition(x, y); + OnPointerPositionChanged(x, y); + } + + public virtual void OnPointerPositionChanged(double x, double y) + { + } + + public void GetPointerScreenPosition(out double x, out double y) + { + _trackingAdapter.GetPointerScreenPosition(out x, out y); + } + + public void SetPointerScreenPosition(double x, double y) + { + _trackingAdapter.SetPointerScreenPosition(x, y); + OnPointerScreenPositionChanged(x, y); + } + + public virtual void OnPointerScreenPositionChanged(double x, double y) + { + } + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + field = value; + OnPropertyChanged(propertyName ?? string.Empty); + return true; + } + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/src/Dock.Avalonia/Controls/ManagedHostWindow.cs b/src/Dock.Avalonia/Controls/ManagedHostWindow.cs new file mode 100644 index 000000000..797eb1880 --- /dev/null +++ b/src/Dock.Avalonia/Controls/ManagedHostWindow.cs @@ -0,0 +1,270 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +using System; +using Avalonia; +using Dock.Avalonia.Internal; +using Dock.Model; +using Dock.Model.Controls; +using Dock.Model.Core; + +namespace Dock.Avalonia.Controls; + +/// +/// Managed in-app host window implementation. +/// +public sealed class ManagedHostWindow : IHostWindow +{ + private readonly ManagedHostWindowState _hostWindowState; + private ManagedDockWindowDocument? _document; + private ManagedWindowDock? _dock; + private IDock? _layout; + private string? _title; + private double _x; + private double _y; + private double _width = 400; + private double _height = 300; + private bool _closed; + private bool _lastCloseCanceled; + + /// + /// Gets the current z-index for managed ordering. + /// + public int ManagedZIndex => _document?.MdiZIndex ?? 0; + + internal bool LastCloseCanceled => _lastCloseCanceled; + + /// + public IHostWindowState HostWindowState => _hostWindowState; + + /// + public bool IsTracked { get; set; } + + /// + public IDockWindow? Window { get; set; } + + public ManagedHostWindow() + { + _hostWindowState = new ManagedHostWindowState(new DockManager(new DockService()), this); + } + + /// + public void Present(bool isDialog) + { + if (Window?.Factory is not { } factory) + { + return; + } + + _dock = ManagedWindowRegistry.GetOrCreateDock(factory); + _layout ??= Window.Layout; + _title ??= Window.Title; + + if (_document is null) + { + _document = new ManagedDockWindowDocument(Window); + _document.Title = _title ?? _document.Title; + _document.MdiBounds = new DockRect(_x, _y, _width, _height); + if (_layout is { } root) + { + _document.Content = ManagedDockWindowDocumentContent.Create(root); + } + } + + _dock.AddWindow(_document); + Window.Host = this; + IsTracked = true; + _closed = false; + + if (!factory.HostWindows.Contains(this)) + { + factory.HostWindows.Add(this); + } + + factory.OnWindowOpened(Window); + } + + /// + public void Exit() + { + if (_closed) + { + return; + } + + _lastCloseCanceled = false; + + if (Window is { } window) + { + if (window.Factory is { } factory) + { + if (factory.OnWindowClosing(window) == false) + { + _lastCloseCanceled = true; + return; + } + } + else if (!window.OnClose()) + { + _lastCloseCanceled = true; + return; + } + + if (IsTracked) + { + window.Save(); + + if (window.Layout is IDock root && root.Close.CanExecute(null)) + { + root.Close.Execute(null); + } + } + } + + _closed = true; + + if (Window?.Factory is { } windowFactory) + { + windowFactory.HostWindows.Remove(this); + + if (Window is { } && Window.Layout is { }) + { + windowFactory.CloseWindow(Window); + } + + windowFactory.OnWindowClosed(Window); + + if (Window is { } && IsTracked) + { + windowFactory.RemoveWindow(Window); + } + } + + if (_dock is { } && _document is { }) + { + _dock.RemoveWindow(_document); + _document.Dispose(); + _document = null; + } + + if (Window is { }) + { + Window.Host = null; + } + + IsTracked = false; + } + + /// + public void SetPosition(double x, double y) + { + if (double.IsNaN(x) || double.IsNaN(y)) + { + return; + } + + _x = x; + _y = y; + UpdateBounds(); + } + + /// + public void GetPosition(out double x, out double y) + { + x = _x; + y = _y; + } + + /// + public void SetSize(double width, double height) + { + if (double.IsNaN(width) || double.IsNaN(height)) + { + return; + } + + _width = width; + _height = height; + UpdateBounds(); + } + + /// + public void GetSize(out double width, out double height) + { + width = _width; + height = _height; + } + + /// + public void SetTitle(string? title) + { + _title = title; + if (_document is { }) + { + _document.Title = title ?? string.Empty; + } + } + + /// + public void SetLayout(IDock layout) + { + _layout = layout; + if (_document is { } document) + { + document.Content = ManagedDockWindowDocumentContent.Create(layout); + } + } + + /// + public void SetActive() + { + if (_dock is { } && _document is { }) + { + _dock.ActiveDockable = _document; + } + } + + private void UpdateBounds() + { + if (_document is not null) + { + _document.MdiBounds = new DockRect(_x, _y, _width, _height); + } + } + + internal void UpdateBoundsFromDocument(DockRect bounds) + { + _x = bounds.X; + _y = bounds.Y; + _width = bounds.Width; + _height = bounds.Height; + } + + internal void ProcessDrag(PixelPoint screenPoint, EventType eventType) + { + _hostWindowState.Process(screenPoint, eventType); + } + + private static class ManagedDockWindowDocumentContent + { + public static object? Create(IDock? layout) + { + if (layout is null) + { + return null; + } + + if (layout is not IRootDock rootDock) + { + return null; + } + + return new DockControl + { + Layout = rootDock, + InitializeFactory = false, + InitializeLayout = false, + EnableManagedWindowLayer = false + }; + } + } +} diff --git a/src/Dock.Avalonia/Controls/ManagedWindowDock.cs b/src/Dock.Avalonia/Controls/ManagedWindowDock.cs new file mode 100644 index 000000000..8594eb57b --- /dev/null +++ b/src/Dock.Avalonia/Controls/ManagedWindowDock.cs @@ -0,0 +1,198 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +using System; +using System.Collections.Generic; +using System.Windows.Input; +using Avalonia.Collections; +using Dock.Model.Core; + +namespace Dock.Avalonia.Controls; + +/// +/// Dock used to host managed floating windows. +/// +public sealed class ManagedWindowDock : ManagedDockableBase, IDock +{ + private static readonly ICommand s_noOpCommand = new NoOpCommand(); + private IList? _visibleDockables; + private IDockable? _activeDockable; + private IDockable? _defaultDockable; + private IDockable? _focusedDockable; + private bool _isActive; + private int _openedDockablesCount; + private bool _canCloseLastDockable = true; + private bool _enableGlobalDocking = true; + + /// + /// Initializes a new instance of the class. + /// + public ManagedWindowDock() + { + _visibleDockables = new AvaloniaList(); + IsCollapsable = false; + } + + public IList? VisibleDockables + { + get => _visibleDockables; + set => SetProperty(ref _visibleDockables, value); + } + + public IDockable? ActiveDockable + { + get => _activeDockable; + set + { + if (ReferenceEquals(_activeDockable, value)) + { + return; + } + + var previousWindow = _activeDockable as ManagedDockWindowDocument; + var currentWindow = value as ManagedDockWindowDocument; + + if (SetProperty(ref _activeDockable, value)) + { + Factory?.InitActiveDockable(value, this); + NotifyWindowActivation(previousWindow, currentWindow); + } + } + } + + public IDockable? DefaultDockable + { + get => _defaultDockable; + set => SetProperty(ref _defaultDockable, value); + } + + public IDockable? FocusedDockable + { + get => _focusedDockable; + set + { + if (SetProperty(ref _focusedDockable, value)) + { + Factory?.OnFocusedDockableChanged(value); + } + } + } + + public bool IsActive + { + get => _isActive; + set => SetProperty(ref _isActive, value); + } + + public int OpenedDockablesCount + { + get => _openedDockablesCount; + set => SetProperty(ref _openedDockablesCount, value); + } + + public bool CanCloseLastDockable + { + get => _canCloseLastDockable; + set => SetProperty(ref _canCloseLastDockable, value); + } + + public bool CanGoBack => false; + + public bool CanGoForward => false; + + public ICommand GoBack => s_noOpCommand; + + public ICommand GoForward => s_noOpCommand; + + public ICommand Navigate => s_noOpCommand; + + public ICommand Close => s_noOpCommand; + + public bool EnableGlobalDocking + { + get => _enableGlobalDocking; + set => SetProperty(ref _enableGlobalDocking, value); + } + + private void NotifyWindowActivation(ManagedDockWindowDocument? previous, ManagedDockWindowDocument? current) + { + if (Factory is null) + { + return; + } + + if (previous?.Window is { } previousWindow) + { + Factory.OnWindowDeactivated(previousWindow); + if (previousWindow.Layout?.ActiveDockable is { } previousDockable) + { + Factory.OnDockableDeactivated(previousDockable); + } + } + + if (current?.Window is { } currentWindow) + { + Factory.OnWindowActivated(currentWindow); + if (currentWindow.Layout?.ActiveDockable is { } currentDockable) + { + Factory.OnDockableActivated(currentDockable); + } + } + } + + /// + /// Adds a managed dock window to the dock. + /// + public void AddWindow(ManagedDockWindowDocument window) + { + if (window is null) + { + return; + } + + VisibleDockables ??= new AvaloniaList(); + + if (!VisibleDockables.Contains(window)) + { + window.Owner = this; + window.Factory = Factory; + VisibleDockables.Add(window); + OpenedDockablesCount = VisibleDockables.Count; + ActiveDockable = window; + } + } + + /// + /// Removes a managed dock window from the dock. + /// + public void RemoveWindow(ManagedDockWindowDocument window) + { + if (VisibleDockables is null || window is null) + { + return; + } + + if (VisibleDockables.Remove(window)) + { + OpenedDockablesCount = VisibleDockables.Count; + if (ReferenceEquals(ActiveDockable, window)) + { + ActiveDockable = VisibleDockables.Count > 0 ? VisibleDockables[VisibleDockables.Count - 1] : null; + } + } + } + + private sealed class NoOpCommand : ICommand + { + public event EventHandler? CanExecuteChanged + { + add { } + remove { } + } + + public bool CanExecute(object? parameter) => false; + + public void Execute(object? parameter) + { + } + } +} diff --git a/src/Dock.Avalonia/Controls/ManagedWindowLayer.cs b/src/Dock.Avalonia/Controls/ManagedWindowLayer.cs new file mode 100644 index 000000000..701111b24 --- /dev/null +++ b/src/Dock.Avalonia/Controls/ManagedWindowLayer.cs @@ -0,0 +1,467 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; +using Dock.Avalonia.Mdi; +using Dock.Model.Controls; +using Dock.Model.Core; + +namespace Dock.Avalonia.Controls; + +/// +/// Managed window layer hosting floating windows inside the main visual tree. +/// +[TemplatePart("PART_OverlayCanvas", typeof(Canvas))] +public sealed class ManagedWindowLayer : TemplatedControl +{ + /// + /// Defines the property. + /// + public static readonly StyledProperty DockProperty = + AvaloniaProperty.Register(nameof(Dock)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LayoutManagerProperty = + AvaloniaProperty.Register(nameof(LayoutManager), ClassicMdiLayoutManager.Instance); + + private readonly Dictionary _overlays = new(); + private readonly Dictionary _windowSubscriptions = new(); + private INotifyPropertyChanged? _dockSubscription; + private INotifyCollectionChanged? _dockablesSubscription; + private Canvas? _overlayCanvas; + private Point? _cachedWindowContentOffset; + + /// + /// Gets or sets the dock that owns managed windows. + /// + public IDock? Dock + { + get => GetValue(DockProperty); + set => SetValue(DockProperty, value); + } + + /// + /// Gets or sets the layout manager used for MDI layout. + /// + public IMdiLayoutManager? LayoutManager + { + get => GetValue(LayoutManagerProperty); + set => SetValue(LayoutManagerProperty, value); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _overlayCanvas = e.NameScope.Find("PART_OverlayCanvas"); + _cachedWindowContentOffset = null; + AttachDock(Dock); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DockProperty) + { + AttachDock(change.GetNewValue()); + } + } + + /// + /// Attempts to locate the managed window layer for a visual. + /// + public static ManagedWindowLayer? TryGetLayer(Visual visual) + { + if (visual is null) + { + return null; + } + + var dockControl = visual as DockControl ?? visual.FindAncestorOfType(); + if (dockControl is { }) + { + var layer = dockControl.GetVisualDescendants().OfType().FirstOrDefault(); + if (layer is not null) + { + return layer; + } + } + + if (visual.GetVisualRoot() is not Visual root) + { + return null; + } + + return root.GetVisualDescendants().OfType().FirstOrDefault(); + } + + /// + /// Shows or updates an overlay element. + /// + public void ShowOverlay(string key, Control control, Rect bounds, bool hitTestVisible) + { + ShowOverlay(key, control, bounds.Position, bounds.Size, hitTestVisible); + } + + /// + /// Shows or updates an overlay element at the given position. + /// + public void ShowOverlay(string key, Control control, Point position, Size? size, bool hitTestVisible) + { + if (_overlayCanvas is null) + { + ApplyTemplate(); + } + + if (_overlayCanvas is null) + { + return; + } + + if (!_overlays.TryGetValue(key, out var existing)) + { + existing = control; + _overlays[key] = existing; + _overlayCanvas.Children.Add(existing); + } + + existing.IsHitTestVisible = hitTestVisible; + if (size.HasValue) + { + existing.Width = size.Value.Width; + existing.Height = size.Value.Height; + } + else + { + existing.Width = double.NaN; + existing.Height = double.NaN; + } + Canvas.SetLeft(existing, position.X); + Canvas.SetTop(existing, position.Y); + } + + /// + /// Removes an overlay element. + /// + public void HideOverlay(string key) + { + if (_overlayCanvas is null) + { + return; + } + + if (_overlays.TryGetValue(key, out var control)) + { + _overlayCanvas.Children.Remove(control); + _overlays.Remove(key); + } + } + + internal bool TryGetWindowContentOffset(out Point offset) + { + offset = default; + + if (_cachedWindowContentOffset.HasValue) + { + offset = _cachedWindowContentOffset.Value; + return true; + } + + var window = this.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.IsVisible); + + if (window is { } && window.TryGetContentOffset(out offset)) + { + _cachedWindowContentOffset = offset; + return true; + } + + if (TryMeasureWindowContentOffset(out offset)) + { + _cachedWindowContentOffset = offset; + return true; + } + + return false; + } + + private bool TryMeasureWindowContentOffset(out Point offset) + { + offset = default; + + if (_overlayCanvas is null) + { + ApplyTemplate(); + } + + if (_overlayCanvas is null) + { + return false; + } + + var probe = new MdiDocumentWindow + { + Width = 300, + Height = 200, + Opacity = 0, + IsHitTestVisible = false, + DataContext = new OffsetProbeDockable { Title = string.Empty } + }; + + ShowOverlay("ManagedWindowOffsetProbe", probe, new Point(0, 0), new Size(300, 200), false); + probe.ApplyTemplate(); + probe.Measure(new Size(300, 200)); + probe.Arrange(new Rect(new Point(0, 0), new Size(300, 200))); + + var measured = probe.TryGetContentOffset(out offset); + HideOverlay("ManagedWindowOffsetProbe"); + return measured; + } + + private sealed class OffsetProbeDockable : ManagedDockableBase + { + } + + private void AttachDock(IDock? dock) + { + DetachDock(); + + if (dock is null) + { + return; + } + + DataContext = dock; + + if (dock is INotifyPropertyChanged propertyChanged) + { + _dockSubscription = propertyChanged; + _dockSubscription.PropertyChanged += DockPropertyChanged; + } + + AttachDockablesCollection(dock.VisibleDockables as INotifyCollectionChanged); + TrackDockables(dock.VisibleDockables); + UpdateZOrder(); + } + + private void DetachDock() + { + ClearDockableSubscriptions(); + + if (_dockSubscription is not null) + { + _dockSubscription.PropertyChanged -= DockPropertyChanged; + _dockSubscription = null; + } + + if (_dockablesSubscription is not null) + { + _dockablesSubscription.CollectionChanged -= DockablesCollectionChanged; + _dockablesSubscription = null; + } + + DataContext = null; + } + + private void DockPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (Dock is null) + { + return; + } + + if (e.PropertyName == nameof(IDock.VisibleDockables)) + { + AttachDockablesCollection(Dock.VisibleDockables as INotifyCollectionChanged); + TrackDockables(Dock.VisibleDockables); + UpdateZOrder(); + return; + } + + if (e.PropertyName == nameof(IDock.ActiveDockable)) + { + UpdateZOrder(); + } + } + + private void AttachDockablesCollection(INotifyCollectionChanged? collection) + { + if (_dockablesSubscription is not null) + { + _dockablesSubscription.CollectionChanged -= DockablesCollectionChanged; + _dockablesSubscription = null; + } + + if (collection is null) + { + return; + } + + _dockablesSubscription = collection; + _dockablesSubscription.CollectionChanged += DockablesCollectionChanged; + } + + private void DockablesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewItems is { } added) + { + foreach (var item in added.OfType()) + { + AttachDockableSubscription(item); + } + } + break; + case NotifyCollectionChangedAction.Remove: + if (e.OldItems is { } removed) + { + foreach (var item in removed.OfType()) + { + DetachDockableSubscription(item); + } + } + break; + case NotifyCollectionChangedAction.Reset: + TrackDockables(Dock?.VisibleDockables); + break; + case NotifyCollectionChangedAction.Replace: + if (e.OldItems is { } replacedOld) + { + foreach (var item in replacedOld.OfType()) + { + DetachDockableSubscription(item); + } + } + if (e.NewItems is { } replacedNew) + { + foreach (var item in replacedNew.OfType()) + { + AttachDockableSubscription(item); + } + } + break; + } + + UpdateZOrder(); + } + + private void UpdateZOrder() + { + if (Dock?.VisibleDockables is null) + { + return; + } + + var documents = Dock.VisibleDockables.OfType().ToList(); + var manager = LayoutManager ?? ClassicMdiLayoutManager.Instance; + var active = Dock.ActiveDockable as IMdiDocument; + + var topmostDocuments = documents.Where(IsTopmost).ToList(); + if (topmostDocuments.Count == 0) + { + manager.UpdateZOrder(documents, active); + return; + } + + var normalDocuments = documents.Where(document => !IsTopmost(document)).ToList(); + var activeNormal = active is not null && normalDocuments.Contains(active) ? active : null; + var activeTopmost = active is not null && topmostDocuments.Contains(active) ? active : null; + + manager.UpdateZOrder(normalDocuments, activeNormal); + manager.UpdateZOrder(topmostDocuments, activeTopmost); + + var offset = normalDocuments.Count; + foreach (var document in topmostDocuments) + { + document.MdiZIndex += offset; + } + } + + private static bool IsTopmost(IMdiDocument document) + { + return document is ManagedDockWindowDocument { Window: { Topmost: true } }; + } + + private void TrackDockables(IList? dockables) + { + ClearDockableSubscriptions(); + + if (dockables is null) + { + return; + } + + foreach (var document in dockables.OfType()) + { + AttachDockableSubscription(document); + } + } + + private void ClearDockableSubscriptions() + { + if (_windowSubscriptions.Count == 0) + { + return; + } + + foreach (var document in _windowSubscriptions.Keys.ToList()) + { + DetachDockableSubscription(document); + } + + _windowSubscriptions.Clear(); + } + + private void AttachDockableSubscription(ManagedDockWindowDocument document) + { + if (_windowSubscriptions.ContainsKey(document)) + { + return; + } + + if (document.Window is not INotifyPropertyChanged window) + { + return; + } + + PropertyChangedEventHandler handler = (_, e) => + { + if (string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == nameof(IDockWindow.Topmost)) + { + UpdateZOrder(); + } + }; + + window.PropertyChanged += handler; + _windowSubscriptions[document] = handler; + } + + private void DetachDockableSubscription(ManagedDockWindowDocument document) + { + if (!_windowSubscriptions.TryGetValue(document, out var handler)) + { + return; + } + + if (document.Window is INotifyPropertyChanged window) + { + window.PropertyChanged -= handler; + } + + _windowSubscriptions.Remove(document); + } +} diff --git a/src/Dock.Avalonia/Controls/MdiDocumentWindow.axaml.cs b/src/Dock.Avalonia/Controls/MdiDocumentWindow.axaml.cs index 37a29f7ea..78907d8a6 100644 --- a/src/Dock.Avalonia/Controls/MdiDocumentWindow.axaml.cs +++ b/src/Dock.Avalonia/Controls/MdiDocumentWindow.axaml.cs @@ -17,6 +17,7 @@ using Dock.Avalonia.Mdi; using Dock.Model.Controls; using Dock.Model.Core; +using Dock.Settings; namespace Dock.Avalonia.Controls; @@ -91,6 +92,9 @@ public class MdiDocumentWindow : TemplatedControl private Point _dragStartPoint; private Rect _dragStartBounds; private MdiResizeDirection _resizeDirection; + private Point _lastPointerPosition; + private bool _managedWindowDragActive; + private IDockWindow? _managedDragWindow; /// /// Gets or sets tab icon template. @@ -172,6 +176,30 @@ public MdiDocumentWindow() UpdatePseudoClasses(IsActive, MdiState); } + internal bool TryGetContentOffset(out Point offset) + { + offset = default; + + if (_contentBorder is null) + { + ApplyTemplate(); + } + + if (_contentBorder is null) + { + return false; + } + + var origin = _contentBorder.TranslatePoint(new Point(0, 0), this); + if (!origin.HasValue) + { + return false; + } + + offset = origin.Value; + return true; + } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -388,12 +416,29 @@ private void BeginDrag(PointerPressedEventArgs e) return; } + if (GetValue(DockProperties.IsDragEnabledProperty) != true) + { + return; + } + + if (_currentDocument is IDockable { CanDrag: false }) + { + return; + } + + if (!TryBeginManagedWindowDrag()) + { + return; + } + _isDragging = true; _dragStartPoint = GetPointerPosition(e); + _lastPointerPosition = _dragStartPoint; _dragStartBounds = ToAvaloniaRect(_currentDocument.MdiBounds); _capturedPointer = e.Pointer; e.Pointer.Capture(this); e.Handled = true; + ProcessManagedWindowDrag(_lastPointerPosition, EventType.Pressed); } private void PointerMovedHandler(object? sender, PointerEventArgs e) @@ -412,11 +457,22 @@ private void PointerMovedHandler(object? sender, PointerEventArgs e) private void PointerReleasedHandler(object? sender, PointerReleasedEventArgs e) { + if (_isDragging) + { + _lastPointerPosition = GetPointerPosition(e); + ProcessManagedWindowDrag(_lastPointerPosition, EventType.Released); + } + EndDragOrResize(); } private void PointerCaptureLostHandler(object? sender, PointerCaptureLostEventArgs e) { + if (_isDragging) + { + ProcessManagedWindowDrag(_lastPointerPosition, EventType.CaptureLost); + } + EndDragOrResize(); } @@ -428,12 +484,20 @@ private void DragMove(PointerEventArgs e) } var position = GetPointerPosition(e); + _lastPointerPosition = position; var delta = position - _dragStartPoint; var manager = GetLayoutManager(out var entries, out var finalSize); var bounds = manager.GetDragBounds(_currentDocument, _dragStartBounds, delta, finalSize, entries); + if (DockSettings.EnableWindowMagnetism && entries.Count > 0) + { + bounds = ApplyWindowMagnetism(bounds, _currentDocument, entries); + bounds = ClampToAvailableSize(bounds, entries, finalSize); + } _currentDocument.MdiBounds = ToDockRect(bounds); InvalidateMdiLayout(); e.Handled = true; + ProcessManagedWindowDrag(_lastPointerPosition, EventType.Moved); + NotifyManagedWindowDrag(); } private void ResizeMove(PointerEventArgs e) @@ -454,6 +518,11 @@ private void ResizeMove(PointerEventArgs e) private void EndDragOrResize() { + if (_managedWindowDragActive) + { + NotifyManagedWindowDragEnd(); + } + _isDragging = false; _isResizing = false; _resizeDirection = MdiResizeDirection.None; @@ -464,6 +533,76 @@ private void EndDragOrResize() } } + private void ProcessManagedWindowDrag(Point localPosition, EventType eventType) + { + if (_currentDocument is not ManagedDockWindowDocument managedDocument) + { + return; + } + + if (managedDocument.Window?.Host is not ManagedHostWindow managedHost) + { + return; + } + + var relativeVisual = this.FindAncestorOfType() as Visual ?? this; + var screenPoint = relativeVisual.PointToScreen(localPosition); + managedHost.ProcessDrag(screenPoint, eventType); + } + + private bool TryBeginManagedWindowDrag() + { + if (_currentDocument is not ManagedDockWindowDocument managedDocument) + { + _managedWindowDragActive = false; + _managedDragWindow = null; + return true; + } + + var window = managedDocument.Window; + if (window?.Factory is not { } factory) + { + _managedWindowDragActive = false; + _managedDragWindow = null; + return true; + } + + if (!factory.OnWindowMoveDragBegin(window)) + { + _managedWindowDragActive = false; + _managedDragWindow = null; + return false; + } + + if (DockSettings.BringWindowsToFrontOnDrag) + { + WindowActivationHelper.ActivateAllWindows(factory, this); + } + + _managedWindowDragActive = true; + _managedDragWindow = window; + return true; + } + + private void NotifyManagedWindowDrag() + { + if (_managedWindowDragActive && _managedDragWindow?.Factory is { } factory) + { + factory.OnWindowMoveDrag(_managedDragWindow); + } + } + + private void NotifyManagedWindowDragEnd() + { + if (_managedWindowDragActive && _managedDragWindow?.Factory is { } factory) + { + factory.OnWindowMoveDragEnd(_managedDragWindow); + } + + _managedWindowDragActive = false; + _managedDragWindow = null; + } + private void AttachResizeHandle(Control? handle, MdiResizeDirection direction) { if (handle is null) @@ -633,6 +772,127 @@ private static DockRect ToDockRect(Rect bounds) return new DockRect(bounds.X, bounds.Y, bounds.Width, bounds.Height); } + private static Rect ApplyWindowMagnetism(Rect bounds, IMdiDocument current, IReadOnlyList entries) + { + var snap = DockSettings.WindowMagnetDistance; + if (snap <= 0) + { + return bounds; + } + + var rect = bounds; + var x = rect.X; + var y = rect.Y; + + foreach (var entry in entries) + { + if (ReferenceEquals(entry.Document, current)) + { + continue; + } + + if (entry.Document.MdiState != MdiWindowState.Normal) + { + continue; + } + + var other = ToAvaloniaRect(entry.Document.MdiBounds); + if (!IsValidBounds(other)) + { + continue; + } + + var verticalOverlap = rect.Top < other.Bottom && rect.Bottom > other.Top; + var horizontalOverlap = rect.Left < other.Right && rect.Right > other.Left; + + if (verticalOverlap) + { + if (Math.Abs(rect.Left - other.Right) <= snap) + { + x = other.Right; + } + else if (Math.Abs(rect.Right - other.Left) <= snap) + { + x = other.Left - rect.Width; + } + } + + if (horizontalOverlap) + { + if (Math.Abs(rect.Top - other.Bottom) <= snap) + { + y = other.Bottom; + } + else if (Math.Abs(rect.Bottom - other.Top) <= snap) + { + y = other.Top - rect.Height; + } + } + } + + if (x == rect.X && y == rect.Y) + { + return rect; + } + + return new Rect(x, y, rect.Width, rect.Height); + } + + private static Rect ClampToAvailableSize(Rect bounds, IReadOnlyList entries, Size finalSize) + { + var availableSize = GetAvailableSize(entries, finalSize); + var maxX = Math.Max(0, availableSize.Width - bounds.Width); + var maxY = Math.Max(0, availableSize.Height - bounds.Height); + + var x = bounds.X; + if (x < 0) + { + x = 0; + } + else if (x > maxX) + { + x = maxX; + } + + var y = bounds.Y; + if (y < 0) + { + y = 0; + } + else if (y > maxY) + { + y = maxY; + } + + return new Rect(x, y, bounds.Width, bounds.Height); + } + + private static Size GetAvailableSize(IReadOnlyList entries, Size finalSize) + { + var minimizedCount = 0; + foreach (var entry in entries) + { + if (entry.Document.MdiState == MdiWindowState.Minimized) + { + minimizedCount++; + } + } + + var reservedHeight = minimizedCount > 0 + ? MdiLayoutDefaults.MinimizedHeight + MdiLayoutDefaults.MinimizedSpacing + : 0; + + return new Size(finalSize.Width, Math.Max(0, finalSize.Height - reservedHeight)); + } + + private static bool IsValidBounds(Rect bounds) + { + return !double.IsNaN(bounds.Width) + && !double.IsNaN(bounds.Height) + && bounds.Width > 0 + && bounds.Height > 0; + } + private bool IsWithinButton(Control? source) { if (_header is null || source is null) diff --git a/src/Dock.Avalonia/Controls/PinnedDockControl.axaml.cs b/src/Dock.Avalonia/Controls/PinnedDockControl.axaml.cs index 2ca35d394..31693d7e6 100644 --- a/src/Dock.Avalonia/Controls/PinnedDockControl.axaml.cs +++ b/src/Dock.Avalonia/Controls/PinnedDockControl.axaml.cs @@ -45,6 +45,8 @@ public Alignment PinnedDockAlignment private GridSplitter? _pinnedDockSplitter; private PinnedDockWindow? _window; private Window? _ownerWindow; + private ManagedWindowLayer? _managedLayer; + private Control? _managedPinnedHost; private IDockable? _lastPinnedDockable; private double _lastPinnedWidth = double.NaN; private double _lastPinnedHeight = double.NaN; @@ -170,13 +172,18 @@ private void UpdateWindow() return; } - if (DataContext is not IRootDock root || root.PinnedDock is null) { CloseWindow(); return; } + if (DockSettings.UseManagedWindows) + { + UpdateManagedWindow(root); + return; + } + if (!root.PinnedDock.IsEmpty) { if (_window is null) @@ -221,6 +228,14 @@ private void UpdateWindow() private void CloseWindow() { + if (_managedLayer is not null) + { + _managedLayer.HideOverlay("PinnedDock"); + _managedLayer = null; + } + + _managedPinnedHost = null; + if (_window is not null) { _window.Close(); @@ -244,6 +259,52 @@ private void CloseWindow() } + private void UpdateManagedWindow(IRootDock root) + { + if (root.PinnedDock is null || root.PinnedDock.IsEmpty) + { + CloseWindow(); + return; + } + + _managedLayer = ManagedWindowLayer.TryGetLayer(this); + if (_managedLayer is null) + { + return; + } + + if (_managedPinnedHost is null) + { + _managedPinnedHost = new ToolDockControl + { + DataContext = root.PinnedDock + }; + } + + var point = _pinnedDock!.PointToScreen(new Point()); + var bounds = GetManagedBounds(_managedLayer, point, _pinnedDock.Bounds.Size); + _managedLayer.ShowOverlay("PinnedDock", _managedPinnedHost, bounds, true); + + if (_pinnedDock.Opacity != 0) + { + _pinnedDock.Opacity = 0; + _pinnedDock.IsHitTestVisible = false; + } + } + + private static Rect GetManagedBounds(ManagedWindowLayer layer, PixelPoint screenPoint, Size size) + { + if (layer.GetVisualRoot() is not TopLevel topLevel) + { + return new Rect(0, 0, size.Width, size.Height); + } + + var clientPoint = topLevel.PointToClient(screenPoint); + var layerOrigin = layer.TranslatePoint(new Point(0, 0), topLevel) ?? new Point(0, 0); + var local = new Point(clientPoint.X - layerOrigin.X, clientPoint.Y - layerOrigin.Y); + return new Rect(local, size); + } + private void ApplyPinnedDockSize() { if (_isResizingPinnedDock) diff --git a/src/Dock.Avalonia/Controls/ToolChromeControl.axaml.cs b/src/Dock.Avalonia/Controls/ToolChromeControl.axaml.cs index a6922936e..df9731866 100644 --- a/src/Dock.Avalonia/Controls/ToolChromeControl.axaml.cs +++ b/src/Dock.Avalonia/Controls/ToolChromeControl.axaml.cs @@ -223,8 +223,11 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) AttachToWindow(); - var maximizeRestoreButton = e.NameScope.Get public static bool UsePinnedDockWindow = false; + + /// + /// Use managed (in-app) windows instead of native OS windows for floating windows. + /// + public static bool UseManagedWindows = false; /// /// Floating windows use the main window as their owner so they stay in front. diff --git a/src/Dock.Settings/DockSettingsOptions.cs b/src/Dock.Settings/DockSettingsOptions.cs index 0ece3686c..cf5aa3a28 100644 --- a/src/Dock.Settings/DockSettingsOptions.cs +++ b/src/Dock.Settings/DockSettingsOptions.cs @@ -30,6 +30,11 @@ public class DockSettingsOptions /// public bool? UsePinnedDockWindow { get; set; } + /// + /// Optional managed window hosting flag. + /// + public bool? UseManagedWindows { get; set; } + /// /// Optional floating window owner flag. /// diff --git a/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs b/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs new file mode 100644 index 000000000..6e13626b3 --- /dev/null +++ b/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs @@ -0,0 +1,1794 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.VisualTree; +using Dock.Avalonia.Converters; +using Dock.Avalonia.Controls; +using Dock.Avalonia.Internal; +using Dock.Avalonia.Mdi; +using Dock.Model; +using Dock.Model.Avalonia; +using Dock.Model.Avalonia.Controls; +using Dock.Model.Avalonia.Core; +using Dock.Model.Controls; +using Dock.Model.Core; +using Dock.Settings; +using Xunit; +using AvaloniaPointer = Avalonia.Input.Pointer; + +namespace Dock.Avalonia.HeadlessTests; + +public class ManagedWindowParityTests +{ + private static (ManagedHostWindow Host, DockWindow Window, IRootDock Root) CreateManagedWindow(Factory factory) + { + return CreateManagedWindow(factory, new DockWindow()); + } + + private static (ManagedHostWindow Host, DockWindow Window, IRootDock Root) CreateManagedWindow( + Factory factory, + DockWindow window) + { + var root = factory.CreateRootDock(); + root.Factory = factory; + + window.Factory = factory; + window.Layout = root; + root.Window = window; + + var host = new ManagedHostWindow + { + Window = window + }; + window.Host = host; + host.Present(false); + + return (host, window, root); + } + + [AvaloniaFact] + public void ActiveDockable_Switch_Raises_Window_Activated_Deactivated() + { + var factory = new Factory(); + var managed1 = CreateManagedWindow(factory); + var managed2 = CreateManagedWindow(factory); + var root1 = managed1.Root; + var root2 = managed2.Root; + + var dockable1 = factory.CreateDocument(); + dockable1.Title = "Doc1"; + root1.VisibleDockables = factory.CreateList(dockable1); + root1.ActiveDockable = dockable1; + + var dockable2 = factory.CreateDocument(); + dockable2.Title = "Doc2"; + root2.VisibleDockables = factory.CreateList(dockable2); + root2.ActiveDockable = dockable2; + + var dock = ManagedWindowRegistry.GetOrCreateDock(factory); + var documents = dock.VisibleDockables!.OfType().ToList(); + + var doc1 = documents.Single(d => ReferenceEquals(d.Window, managed1.Window)); + var doc2 = documents.Single(d => ReferenceEquals(d.Window, managed2.Window)); + + var activated = new List(); + var deactivated = new List(); + var dockableActivated = new List(); + var dockableDeactivated = new List(); + + factory.WindowActivated += (_, e) => activated.Add(e.Window!); + factory.WindowDeactivated += (_, e) => deactivated.Add(e.Window!); + factory.DockableActivated += (_, e) => + { + if (e.Dockable is { } dockable) + { + dockableActivated.Add(dockable); + } + }; + factory.DockableDeactivated += (_, e) => + { + if (e.Dockable is { } dockable) + { + dockableDeactivated.Add(dockable); + } + }; + + dock.ActiveDockable = doc1; + + Assert.Contains(managed1.Window, activated); + Assert.Contains(managed2.Window, deactivated); + Assert.Contains(dockable1, dockableActivated); + Assert.Contains(dockable2, dockableDeactivated); + Assert.Equal(doc1, dock.ActiveDockable); + Assert.NotSame(doc2, dock.ActiveDockable); + } + + [AvaloniaFact] + public void Topmost_Window_Receives_Higher_ZIndex() + { + var factory = new Factory(); + var managed1 = CreateManagedWindow(factory); + var managed2 = CreateManagedWindow(factory); + + var dock = ManagedWindowRegistry.GetOrCreateDock(factory); + _ = new ManagedWindowLayer { Dock = dock }; + + var documents = dock.VisibleDockables!.OfType().ToList(); + var doc1 = documents.Single(d => ReferenceEquals(d.Window, managed1.Window)); + var doc2 = documents.Single(d => ReferenceEquals(d.Window, managed2.Window)); + + managed1.Window.Topmost = true; + + Assert.True(doc1.MdiZIndex > doc2.MdiZIndex); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_Tracks_FocusedDockable_Title() + { + var factory = new Factory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + + var doc1 = factory.CreateDocument(); + doc1.Title = "Doc1"; + root.VisibleDockables = factory.CreateList(doc1); + root.ActiveDockable = doc1; + root.FocusedDockable = doc1; + + var window = new DockWindow + { + Factory = factory, + Layout = root + }; + root.Window = window; + + var managedDoc = new ManagedDockWindowDocument(window); + Assert.Equal("Doc1", managedDoc.Title); + + var doc2 = factory.CreateDocument(); + doc2.Title = "Doc2"; + root.VisibleDockables!.Add(doc2); + root.FocusedDockable = doc2; + + Assert.Equal("Doc2", managedDoc.Title); + } + + [AvaloniaFact] + public void ApplyWindowMagnetism_Snaps_To_Adjacent_Window() + { + var original = DockSettings.WindowMagnetDistance; + try + { + DockSettings.WindowMagnetDistance = 10; + + var current = new Document { MdiBounds = new DockRect(95, 0, 50, 50) }; + var other = new Document { MdiBounds = new DockRect(150, 0, 50, 50) }; + + var entries = new List + { + new(new Control(), current), + new(new Control(), other) + }; + + var method = typeof(MdiDocumentWindow).GetMethod("ApplyWindowMagnetism", BindingFlags.NonPublic | BindingFlags.Static); + var bounds = new Rect(95, 0, 50, 50); + var result = (Rect)method!.Invoke(null, new object[] { bounds, current, entries })!; + + Assert.Equal(100, result.X); + } + finally + { + DockSettings.WindowMagnetDistance = original; + } + } + + [AvaloniaFact] + public void BeginDrag_Respects_IsDragEnabled() + { + var doc = new Document { Title = "Doc", CanDrag = true }; + var mdiWindow = new MdiDocumentWindow + { + Width = 200, + Height = 200 + }; + var host = new Window + { + Width = 400, + Height = 300, + Content = mdiWindow + }; + + host.Show(); + try + { + mdiWindow.DataContext = doc; + mdiWindow.ApplyTemplate(); + host.UpdateLayout(); + mdiWindow.UpdateLayout(); + + mdiWindow.SetValue(DockProperties.IsDragEnabledProperty, false); + + var pointer = new AvaloniaPointer(1, PointerType.Mouse, true); + var props = new PointerPointProperties(RawInputModifiers.LeftMouseButton, PointerUpdateKind.LeftButtonPressed); + var args = new PointerPressedEventArgs(mdiWindow, pointer, host, new Point(5, 5), 0, props, KeyModifiers.None, 1); + + var beginDrag = typeof(MdiDocumentWindow).GetMethod("BeginDrag", BindingFlags.Instance | BindingFlags.NonPublic); + beginDrag!.Invoke(mdiWindow, new object[] { args }); + + var isDraggingField = typeof(MdiDocumentWindow).GetField("_isDragging", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.False((bool)isDraggingField!.GetValue(mdiWindow)!); + } + finally + { + host.Close(); + } + } + + [AvaloniaFact] + public void PointerTracking_Uses_ManagedLayer_Coordinates() + { + var original = DockSettings.UseManagedWindows; + DockSettings.UseManagedWindows = true; + + var document = new Document { Title = "Doc" }; + var dockableControl = new DockableControl + { + Width = 120, + Height = 80, + DataContext = document + }; + + var layer = new ManagedWindowLayer + { + Width = 200, + Height = 200, + IsVisible = true + }; + + var header = new Border { Height = 24 }; + + var grid = new Grid + { + RowDefinitions = new RowDefinitions("24,*"), + Children = + { + header, + layer, + dockableControl + } + }; + + Grid.SetRow(layer, 1); + Grid.SetRow(dockableControl, 1); + + var window = new Window + { + Width = 400, + Height = 300, + Content = grid + }; + + try + { + window.Show(); + window.UpdateLayout(); + + var pointer = new AvaloniaPointer(2, PointerType.Mouse, true); + var props = new PointerPointProperties(RawInputModifiers.LeftMouseButton, PointerUpdateKind.LeftButtonPressed); + var dockableOrigin = dockableControl.TranslatePoint(new Point(0, 0), window) ?? new Point(0, 0); + var rootPoint = new Point(dockableOrigin.X + 10, dockableOrigin.Y + 10); + var args = new PointerPressedEventArgs(dockableControl, pointer, window, rootPoint, 0, props, KeyModifiers.None, 1); + + dockableControl.RaiseEvent(args); + + document.GetPointerScreenPosition(out var pointerX, out var pointerY); + var expected = dockableControl.TranslatePoint(new Point(10, 10), layer); + + Assert.True(expected.HasValue); + Assert.Equal(expected.Value.X, pointerX, 3); + Assert.Equal(expected.Value.Y, pointerY, 3); + } + finally + { + window.Close(); + DockSettings.UseManagedWindows = original; + } + } + + [AvaloniaFact] + public void ManagedPreview_Uses_VisualBrush_Proxy() + { + var window = new DockWindow { Title = "Preview" }; + var managedDoc = new ManagedDockWindowDocument(window); + var content = new Panel(); + managedDoc.Content = content; + + DragPreviewContext.IsActive = true; + DragPreviewContext.Dockable = managedDoc; + + try + { + var preview = managedDoc.Build(null); + var border = Assert.IsType(preview); + var brush = Assert.IsType(border.Background); + + Assert.Same(content, brush.Visual); + } + finally + { + DragPreviewContext.Clear(); + } + } + + [AvaloniaFact] + public void ManagedHostWindowDrag_Does_Not_Show_DragPreview() + { + var originalPreview = DockSettings.ShowDockablePreviewOnDrag; + var originalManaged = DockSettings.UseManagedWindows; + DockSettings.ShowDockablePreviewOnDrag = true; + DockSettings.UseManagedWindows = true; + + 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.MdiBounds = new DockRect(0, 0, 200, 150); + + DragPreviewContext.Clear(); + try + { + var state = new ManagedHostWindowState(new DockManager(new DockService()), host); + state.Process(new PixelPoint(10, 10), EventType.Pressed); + state.Process(new PixelPoint(80, 80), EventType.Moved); + + Assert.False(DragPreviewContext.IsActive); + Assert.Null(DragPreviewContext.Dockable); + + state.Process(new PixelPoint(80, 80), EventType.Released); + Assert.False(DragPreviewContext.IsActive); + } + finally + { + DragPreviewContext.Clear(); + DockSettings.ShowDockablePreviewOnDrag = originalPreview; + DockSettings.UseManagedWindows = originalManaged; + } + } + + [AvaloniaFact] + public void ManagedHostWindowDrag_CaptureLost_Does_Not_Activate_DragPreview() + { + var originalPreview = DockSettings.ShowDockablePreviewOnDrag; + var originalManaged = DockSettings.UseManagedWindows; + DockSettings.ShowDockablePreviewOnDrag = true; + DockSettings.UseManagedWindows = true; + + 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.MdiBounds = new DockRect(0, 0, 200, 150); + + DragPreviewContext.Clear(); + try + { + var state = new ManagedHostWindowState(new DockManager(new DockService()), host); + state.Process(new PixelPoint(10, 10), EventType.Pressed); + state.Process(new PixelPoint(80, 80), EventType.Moved); + + Assert.False(DragPreviewContext.IsActive); + + state.Process(new PixelPoint(80, 80), EventType.CaptureLost); + Assert.False(DragPreviewContext.IsActive); + } + finally + { + DragPreviewContext.Clear(); + DockSettings.ShowDockablePreviewOnDrag = originalPreview; + DockSettings.UseManagedWindows = originalManaged; + } + } + + [AvaloniaFact] + public void DragPreviewHelper_Uses_Managed_Layer_Overlay() + { + var originalPreview = DockSettings.ShowDockablePreviewOnDrag; + var originalManaged = DockSettings.UseManagedWindows; + DockSettings.ShowDockablePreviewOnDrag = true; + DockSettings.UseManagedWindows = true; + + var factory = new Factory(); + var document = factory.CreateDocument(); + document.Title = "Doc"; + document.Factory = factory; + + var layer = new ManagedWindowLayer + { + Width = 300, + Height = 200, + IsVisible = true + }; + + var windowHost = new Window + { + Width = 400, + Height = 300, + Content = layer + }; + + windowHost.Show(); + DragPreviewContext.Clear(); + DragPreviewHelper? helper = null; + try + { + layer.ApplyTemplate(); + windowHost.UpdateLayout(); + + ManagedWindowRegistry.RegisterLayer(factory, layer); + + helper = new DragPreviewHelper(); + helper.Show(document, new PixelPoint(50, 60), new PixelPoint(0, 0)); + + windowHost.UpdateLayout(); + + var canvas = layer.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.Name == "PART_OverlayCanvas"); + + Assert.NotNull(canvas); + Assert.Contains(canvas!.Children, child => child is DragPreviewControl); + var preview = canvas.Children.OfType().FirstOrDefault(); + Assert.NotNull(preview); + Assert.True(preview!.Bounds.Width > 0); + Assert.True(preview.Bounds.Height > 0); + + helper.Hide(); + Assert.DoesNotContain(canvas.Children, child => child is DragPreviewControl); + } + finally + { + helper?.Hide(); + DragPreviewContext.Clear(); + ManagedWindowRegistry.UnregisterLayer(factory, layer); + windowHost.Close(); + DockSettings.ShowDockablePreviewOnDrag = originalPreview; + DockSettings.UseManagedWindows = originalManaged; + } + } + + [AvaloniaFact] + public void DragPreviewHelper_Does_Not_Show_For_ManagedWindow() + { + var originalPreview = DockSettings.ShowDockablePreviewOnDrag; + var originalManaged = DockSettings.UseManagedWindows; + DockSettings.ShowDockablePreviewOnDrag = true; + DockSettings.UseManagedWindows = true; + + 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)); + + var layer = new ManagedWindowLayer + { + Dock = dock, + Width = 400, + Height = 300, + IsVisible = true + }; + + var windowHost = new Window + { + Width = 500, + Height = 400, + Content = layer + }; + + windowHost.Show(); + DragPreviewContext.Clear(); + DragPreviewHelper? helper = null; + try + { + layer.ApplyTemplate(); + windowHost.UpdateLayout(); + + ManagedWindowRegistry.RegisterLayer(factory, layer); + + helper = new DragPreviewHelper(); + helper.Show(managedDocument, new PixelPoint(50, 60), new PixelPoint(0, 0)); + + windowHost.UpdateLayout(); + + var canvas = layer.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.Name == "PART_OverlayCanvas"); + + Assert.NotNull(canvas); + Assert.DoesNotContain(canvas!.Children, child => child is DragPreviewControl); + Assert.False(DragPreviewContext.IsActive); + Assert.Null(DragPreviewContext.Dockable); + + helper.Hide(); + Assert.DoesNotContain(canvas.Children, child => child is DragPreviewControl); + } + finally + { + helper?.Hide(); + DragPreviewContext.Clear(); + ManagedWindowRegistry.UnregisterLayer(factory, layer); + windowHost.Close(); + host.Exit(); + DockSettings.ShowDockablePreviewOnDrag = originalPreview; + DockSettings.UseManagedWindows = originalManaged; + } + } + + [AvaloniaFact] + public void DragPreviewHelper_Uses_Managed_Layer_From_Visual_Context() + { + var originalPreview = DockSettings.ShowDockablePreviewOnDrag; + var originalManaged = DockSettings.UseManagedWindows; + DockSettings.ShowDockablePreviewOnDrag = true; + DockSettings.UseManagedWindows = true; + + var factory = new Factory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + + var document = factory.CreateDocument(); + document.Title = "Doc"; + root.VisibleDockables = factory.CreateList(document); + root.ActiveDockable = document; + + var dockControl = new DockControl + { + Layout = root + }; + + var window = new Window + { + Width = 400, + Height = 300, + Content = dockControl + }; + + window.Show(); + DragPreviewContext.Clear(); + try + { + dockControl.ApplyTemplate(); + window.UpdateLayout(); + + var helper = new DragPreviewHelper(); + helper.Show(document, new PixelPoint(50, 60), new PixelPoint(0, 0), dockControl); + + var layer = dockControl.GetVisualDescendants().OfType().FirstOrDefault(); + Assert.NotNull(layer); + + var canvas = layer!.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.Name == "PART_OverlayCanvas"); + + Assert.NotNull(canvas); + Assert.Contains(canvas!.Children, child => child is DragPreviewControl); + window.UpdateLayout(); + var preview = canvas.Children.OfType().FirstOrDefault(); + Assert.NotNull(preview); + Assert.True(preview!.Bounds.Width > 0); + Assert.True(preview.Bounds.Height > 0); + + helper.Hide(); + Assert.DoesNotContain(canvas.Children, child => child is DragPreviewControl); + } + finally + { + DragPreviewContext.Clear(); + window.Close(); + DockSettings.ShowDockablePreviewOnDrag = originalPreview; + DockSettings.UseManagedWindows = originalManaged; + } + } + + [AvaloniaFact] + public void DragPreviewControl_Uses_PreviewContent_When_Set() + { + var preview = new Border { Name = "Preview" }; + var control = new DragPreviewControl + { + PreviewContent = preview, + ShowContent = true, + PreviewContentWidth = 120, + PreviewContentHeight = 80 + }; + + var window = new Window + { + Width = 300, + Height = 200, + Content = control + }; + + window.Show(); + try + { + control.ApplyTemplate(); + window.UpdateLayout(); + control.UpdateLayout(); + + var previewPresenter = control.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.Name == "PART_PreviewContentPresenter"); + var contentPresenter = control.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.Name == "PART_ContentPresenter"); + + Assert.NotNull(previewPresenter); + Assert.Same(preview, previewPresenter!.Content); + Assert.True(previewPresenter.IsVisible); + Assert.NotNull(contentPresenter); + Assert.False(contentPresenter!.IsVisible); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void FloatingDockable_Uses_Active_Managed_Layer_For_Pointer() + { + var originalManaged = DockSettings.UseManagedWindows; + DockSettings.UseManagedWindows = true; + + Window? windowA = null; + Window? windowB = null; + ManagedWindowLayer? layerA = null; + ManagedWindowLayer? layerB = null; + Factory? factory = null; + + try + { + factory = new Factory(); + var rootA = factory.CreateRootDock(); + rootA.Factory = factory; + var rootB = factory.CreateRootDock(); + rootB.Factory = factory; + + var dockControlA = new DockControl { Layout = rootA }; + var dockControlB = new DockControl { Layout = rootB }; + + windowA = new Window + { + Width = 400, + Height = 300, + Position = new PixelPoint(0, 0), + Content = dockControlA + }; + + windowB = new Window + { + Width = 400, + Height = 300, + Position = new PixelPoint(200, 120), + Content = dockControlB + }; + + windowA.Show(); + windowB.Show(); + dockControlA.ApplyTemplate(); + dockControlB.ApplyTemplate(); + windowA.UpdateLayout(); + windowB.UpdateLayout(); + + layerA = dockControlA.GetVisualDescendants().OfType().FirstOrDefault(); + layerB = dockControlB.GetVisualDescendants().OfType().FirstOrDefault(); + Assert.NotNull(layerA); + Assert.NotNull(layerB); + + layerA!.IsVisible = true; + layerB!.IsVisible = true; + + ManagedWindowRegistry.RegisterLayer(factory, layerA); + layerB.IsVisible = true; + + var cachedOffset = new Point(10, 14); + var offsetField = typeof(ManagedWindowLayer) + .GetField("_cachedWindowContentOffset", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(offsetField); + offsetField!.SetValue(layerB, cachedOffset); + + var dockable = new Document { Title = "Doc" }; + var state = new TestDockManagerState(new DockManager(new DockService())); + var point = new Point(50, 60); + var dragOffset = new PixelPoint(-12, -8); + + state.InvokeFloat(point, dockControlB, dockable, factory, dragOffset); + + var translated = dockControlB.TranslatePoint(point, layerB); + Assert.True(translated.HasValue); + var scaling = (dockControlB.GetVisualRoot() as TopLevel) + ?.Screens + ?.ScreenFromVisual(dockControlB) + ?.Scaling ?? 1.0; + var expected = new Point( + translated!.Value.X + dragOffset.X / scaling - cachedOffset.X, + translated.Value.Y + dragOffset.Y / scaling - cachedOffset.Y); + + dockable.GetPointerScreenPosition(out var pointerX, out var pointerY); + + Assert.Equal(expected.X, pointerX, 3); + Assert.Equal(expected.Y, pointerY, 3); + } + finally + { + if (layerA is { } && factory is { }) + { + ManagedWindowRegistry.UnregisterLayer(factory, layerA); + } + + windowA?.Close(); + windowB?.Close(); + DockSettings.UseManagedWindows = originalManaged; + } + } + + [AvaloniaFact] + public void FloatingDockable_From_ManagedWindow_Adjusts_For_ContentOffset() + { + var originalManaged = DockSettings.UseManagedWindows; + DockSettings.UseManagedWindows = true; + + Window? window = null; + ManagedWindowLayer? layer = null; + Factory? factory = null; + + try + { + factory = new Factory(); + var mainRoot = factory.CreateRootDock(); + mainRoot.Factory = factory; + + var dockControlMain = new DockControl { Layout = mainRoot }; + + window = new Window + { + Width = 600, + Height = 400, + Position = new PixelPoint(0, 0), + Content = dockControlMain + }; + + window.Show(); + dockControlMain.ApplyTemplate(); + window.UpdateLayout(); + + layer = dockControlMain.GetVisualDescendants().OfType().FirstOrDefault(); + Assert.NotNull(layer); + layer!.IsVisible = true; + + ManagedWindowRegistry.RegisterLayer(factory, layer); + + var (_, managedWindow, managedRoot) = CreateManagedWindow(factory); + var documentDock = factory.CreateDocumentDock(); + var document = factory.CreateDocument(); + documentDock.VisibleDockables = factory.CreateList(document); + documentDock.ActiveDockable = document; + managedRoot.VisibleDockables = factory.CreateList(documentDock); + managedRoot.ActiveDockable = documentDock; + factory.InitLayout(managedRoot); + + var managedDock = ManagedWindowRegistry.GetOrCreateDock(factory); + var managedDocument = managedDock.VisibleDockables!.OfType() + .Single(doc => ReferenceEquals(doc.Window, managedWindow)); + managedDocument.MdiBounds = new DockRect(50, 70, 260, 180); + + window.UpdateLayout(); + + var innerDockControl = window.GetVisualDescendants() + .OfType() + .FirstOrDefault(control => ReferenceEquals(control.Layout, managedRoot)); + Assert.NotNull(innerDockControl); + + var point = new Point(40, 55); + var dragOffset = new PixelPoint(-6, -4); + var state = new TestDockManagerState(new DockManager(new DockService())); + state.InvokeFloat(point, innerDockControl!, document, factory, dragOffset); + + var managedWindowControl = window.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => ReferenceEquals(candidate.DataContext, managedDocument)); + Assert.NotNull(managedWindowControl); + managedWindowControl!.ApplyTemplate(); + managedWindowControl.UpdateLayout(); + + var contentBorder = managedWindowControl.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.Name == "PART_ContentBorder"); + Assert.NotNull(contentBorder); + var origin = contentBorder!.TranslatePoint(new Point(0, 0), managedWindowControl); + Assert.True(origin.HasValue); + var contentOffset = origin.Value; + + var translated = innerDockControl!.TranslatePoint(point, layer); + Assert.True(translated.HasValue); + var scaling = (innerDockControl.GetVisualRoot() as TopLevel) + ?.Screens + ?.ScreenFromVisual(innerDockControl) + ?.Scaling ?? 1.0; + var expected = new Point( + translated!.Value.X + dragOffset.X / scaling - contentOffset.X, + translated.Value.Y + dragOffset.Y / scaling - contentOffset.Y); + + document.GetPointerScreenPosition(out var pointerX, out var pointerY); + + Assert.Equal(expected.X, pointerX, 3); + Assert.Equal(expected.Y, pointerY, 3); + } + finally + { + if (layer is { } && factory is { }) + { + ManagedWindowRegistry.UnregisterLayer(factory, layer); + } + + window?.Close(); + DockSettings.UseManagedWindows = originalManaged; + } + } + + [AvaloniaFact] + public void DockManagerState_Execute_Uses_Managed_Layer_For_WindowDrop() + { + var originalManaged = DockSettings.UseManagedWindows; + DockSettings.UseManagedWindows = true; + + Window? window = null; + ManagedWindowLayer? layer = null; + Factory? factory = null; + + try + { + factory = new Factory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + + var dockControl = new DockControl { Layout = root }; + + window = new Window + { + Width = 400, + Height = 300, + Position = new PixelPoint(120, 80), + Content = dockControl + }; + + window.Show(); + dockControl.ApplyTemplate(); + window.UpdateLayout(); + + layer = dockControl.GetVisualDescendants().OfType().FirstOrDefault(); + Assert.NotNull(layer); + layer!.IsVisible = true; + + ManagedWindowRegistry.RegisterLayer(factory, layer); + + var manager = new TestDockManager(); + var state = new TestDockManagerState(manager); + var source = factory.CreateDocument(); + var target = factory.CreateDocumentDock(); + var point = new Point(30, 45); + + state.InvokeExecute(point, DockOperation.Window, DragAction.Move, dockControl, source, target); + + var translated = dockControl.TranslatePoint(point, layer); + Assert.True(translated.HasValue); + + Assert.Equal(translated.Value.X, manager.ScreenPosition.X, 3); + Assert.Equal(translated.Value.Y, manager.ScreenPosition.Y, 3); + } + finally + { + if (layer is { } && factory is { }) + { + ManagedWindowRegistry.UnregisterLayer(factory, layer); + } + + window?.Close(); + DockSettings.UseManagedWindows = originalManaged; + } + } + + [AvaloniaFact] + public void ManagedHostWindow_Present_Registers_Window_And_Document() + { + var factory = new Factory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + + var window = new DockWindow + { + Factory = factory, + Layout = root, + Title = "Host" + }; + root.Window = window; + + var host = new ManagedHostWindow + { + Window = window + }; + + var opened = new List(); + factory.WindowOpened += (_, e) => opened.Add(e.Window!); + + host.Present(false); + + Assert.True(host.IsTracked); + Assert.Same(host, window.Host); + Assert.Contains(host, factory.HostWindows); + Assert.Contains(window, opened); + + var dock = ManagedWindowRegistry.GetOrCreateDock(factory); + var managedDocument = dock.VisibleDockables!.OfType() + .Single(document => ReferenceEquals(document.Window, window)); + + Assert.Same(dock, managedDocument.Owner); + Assert.Same(factory, managedDocument.Factory); + Assert.Same(managedDocument, dock.ActiveDockable); + Assert.IsType(managedDocument.Content); + } + + [AvaloniaFact] + public void ManagedHostWindow_Present_Without_Factory_Does_Not_Track() + { + var window = new DockWindow(); + var host = new ManagedHostWindow + { + Window = window + }; + + host.Present(false); + + Assert.False(host.IsTracked); + Assert.Null(window.Host); + } + + [AvaloniaFact] + public void ManagedHostWindow_SetTitle_Updates_Document_And_Window() + { + 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.SetTitle("Updated"); + + Assert.Equal("Updated", managedDocument.Title); + Assert.Equal("Updated", window.Title); + } + + [AvaloniaFact] + public void ManagedHostWindow_Exit_Removes_Window_And_Document() + { + var factory = new Factory(); + var (host, window, _) = CreateManagedWindow(factory); + + var closed = new List(); + factory.WindowClosed += (_, e) => closed.Add(e.Window!); + + host.Exit(); + + Assert.False(host.IsTracked); + Assert.Null(window.Host); + Assert.DoesNotContain(host, factory.HostWindows); + Assert.Contains(window, closed); + + var dock = ManagedWindowRegistry.GetOrCreateDock(factory); + Assert.DoesNotContain( + dock.VisibleDockables!.OfType(), + document => ReferenceEquals(document.Window, window)); + } + + [AvaloniaFact] + public void ManagedHostWindow_Exit_Can_Be_Canceled() + { + var factory = new Factory(); + var (host, window, _) = CreateManagedWindow(factory, new TestDockWindow()); + factory.WindowClosing += (_, e) => e.Cancel = true; + + host.Exit(); + + Assert.True(host.IsTracked); + Assert.Same(host, window.Host); + Assert.Contains(host, factory.HostWindows); + + var dock = ManagedWindowRegistry.GetOrCreateDock(factory); + var managedDocument = dock.VisibleDockables!.OfType() + .Single(document => ReferenceEquals(document.Window, window)); + Assert.Same(managedDocument, dock.ActiveDockable); + } + + [AvaloniaFact] + public void ManagedHostWindow_SetPosition_And_Size_Ignore_NaN() + { + var host = new ManagedHostWindow(); + host.SetPosition(10, 20); + host.SetSize(200, 150); + + host.SetPosition(double.NaN, 30); + host.SetSize(double.NaN, 300); + + host.GetPosition(out var x, out var y); + host.GetSize(out var width, out var height); + + Assert.Equal(10, x); + Assert.Equal(20, y); + Assert.Equal(200, width); + Assert.Equal(150, height); + } + + [AvaloniaFact] + public void ManagedHostWindow_SetLayout_Updates_Content() + { + 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)); + var previousContent = managedDocument.Content; + + var newRoot = factory.CreateRootDock(); + newRoot.Factory = factory; + host.SetLayout(newRoot); + + var newContent = Assert.IsType(managedDocument.Content); + Assert.NotSame(previousContent, newContent); + Assert.Same(newRoot, newContent.Layout); + } + + [AvaloniaFact] + public void ManagedHostWindow_SetActive_Activates_Document() + { + 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)); + + dock.ActiveDockable = null; + host.SetActive(); + + Assert.Same(managedDocument, dock.ActiveDockable); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_Syncs_Title_And_Id_To_Window() + { + var window = new DockWindow { Title = "Original", Id = "win-1" }; + var managedDocument = new ManagedDockWindowDocument(window); + + managedDocument.Title = "Updated"; + managedDocument.Id = "win-2"; + + Assert.Equal("Updated", window.Title); + Assert.Equal("win-2", window.Id); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_Window_Title_Change_Does_Not_Override_Focused_Dockable() + { + var factory = new Factory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + + var document = factory.CreateDocument(); + document.Title = "DocTitle"; + root.VisibleDockables = factory.CreateList(document); + root.ActiveDockable = document; + root.FocusedDockable = document; + + var window = new DockWindow + { + Factory = factory, + Layout = root, + Title = "WindowTitle" + }; + root.Window = window; + + var managedDocument = new ManagedDockWindowDocument(window); + window.Title = "UpdatedWindow"; + + Assert.Equal("DocTitle", managedDocument.Title); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_Updates_Title_When_Window_Title_Changes() + { + var factory = new Factory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + + var window = new DockWindow + { + Factory = factory, + Layout = root, + Title = "Initial" + }; + root.Window = window; + + var managedDocument = new ManagedDockWindowDocument(window); + window.Title = "Updated"; + + Assert.Equal("Updated", managedDocument.Title); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_Updates_Title_When_Layout_Swapped() + { + var factory = new Factory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + + var window = new DockWindow + { + Factory = factory, + Layout = root + }; + root.Window = window; + + var initialDocument = factory.CreateDocument(); + initialDocument.Title = "Doc1"; + root.VisibleDockables = factory.CreateList(initialDocument); + root.FocusedDockable = initialDocument; + + var managedDocument = new ManagedDockWindowDocument(window); + Assert.Equal("Doc1", managedDocument.Title); + + var newRoot = factory.CreateRootDock(); + newRoot.Factory = factory; + var newDocument = factory.CreateDocument(); + newDocument.Title = "Doc2"; + newRoot.VisibleDockables = factory.CreateList(newDocument); + newRoot.FocusedDockable = newDocument; + newRoot.Window = window; + + window.Layout = newRoot; + + Assert.Equal("Doc2", managedDocument.Title); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_Build_Uses_Direct_Content() + { + var window = new DockWindow { Title = "Content" }; + var managedDocument = new ManagedDockWindowDocument(window); + var content = new Border { Name = "DirectContent" }; + managedDocument.Content = content; + + var result = managedDocument.Build(null); + + Assert.Same(content, result); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_Build_Uses_Func_Content() + { + var window = new DockWindow { Title = "Content" }; + var managedDocument = new ManagedDockWindowDocument(window); + managedDocument.Content = new Func(_ => new Border { Name = "FactoryContent" }); + + var result = managedDocument.Build(null); + + var border = Assert.IsType(result); + Assert.Equal("FactoryContent", border.Name); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_Match_Respects_DataType() + { + var window = new DockWindow { Title = "Match" }; + var managedDocument = new ManagedDockWindowDocument(window) + { + DataType = typeof(Border) + }; + + Assert.True(managedDocument.Match(new Border())); + Assert.False(managedDocument.Match(new Panel())); + + managedDocument.DataType = null; + Assert.True(managedDocument.Match(new Panel())); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_Dispose_Detaches_Subscriptions() + { + var factory = new Factory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + + var document = factory.CreateDocument(); + document.Title = "DocTitle"; + root.VisibleDockables = factory.CreateList(document); + root.FocusedDockable = document; + + var window = new DockWindow + { + Factory = factory, + Layout = root, + Title = "WindowTitle" + }; + root.Window = window; + + var managedDocument = new ManagedDockWindowDocument(window); + managedDocument.Content = new Border(); + var initialTitle = managedDocument.Title; + + managedDocument.Dispose(); + + window.Title = "UpdatedWindow"; + document.Title = "UpdatedDoc"; + + Assert.Equal(initialTitle, managedDocument.Title); + Assert.Null(managedDocument.Window); + Assert.Null(managedDocument.Content); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_OnClose_Respects_ManagedHost_Cancellation() + { + var factory = new Factory(); + factory.WindowClosing += (_, e) => e.Cancel = true; + + var (host, window, _) = CreateManagedWindow(factory); + var dock = ManagedWindowRegistry.GetOrCreateDock(factory); + var managedDocument = dock.VisibleDockables!.OfType() + .Single(document => ReferenceEquals(document.Window, window)); + + var closed = managedDocument.OnClose(); + + Assert.False(closed); + Assert.True(host.IsTracked); + Assert.Same(host, window.Host); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_OnClose_Uses_NonManaged_Host() + { + var host = new TestHostWindow(); + var window = new DockWindow + { + Host = host + }; + host.Window = window; + + var managedDocument = new ManagedDockWindowDocument(window); + var closed = managedDocument.OnClose(); + + Assert.True(closed); + Assert.True(host.ExitCalled); + Assert.Null(window.Host); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_MdiBounds_Syncs_To_Host() + { + 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.MdiBounds = new DockRect(10, 20, 300, 200); + + host.GetPosition(out var x, out var y); + host.GetSize(out var width, out var height); + + Assert.Equal(10, x); + Assert.Equal(20, y); + Assert.Equal(300, width); + Assert.Equal(200, height); + Assert.Equal(10, window.X); + Assert.Equal(20, window.Y); + Assert.Equal(300, window.Width); + Assert.Equal(200, window.Height); + } + + [AvaloniaFact] + public void ManagedDockWindowDocument_MdiBounds_Syncs_To_NonManaged_Host() + { + var host = new TestHostWindow(); + var window = new DockWindow + { + Host = host + }; + host.Window = window; + + var managedDocument = new ManagedDockWindowDocument(window); + managedDocument.MdiBounds = new DockRect(12, 24, 320, 240); + + host.GetPosition(out var x, out var y); + host.GetSize(out var width, out var height); + + Assert.Equal(12, x); + Assert.Equal(24, y); + Assert.Equal(320, width); + Assert.Equal(240, height); + Assert.Equal(12, window.X); + Assert.Equal(24, window.Y); + Assert.Equal(320, window.Width); + Assert.Equal(240, window.Height); + } + + [AvaloniaFact] + public void ManagedWindowDock_AddRemove_Updates_Count_And_Active() + { + var factory = new Factory(); + var dock = new ManagedWindowDock { Factory = factory }; + var window1 = new DockWindow { Factory = factory }; + var window2 = new DockWindow { Factory = factory }; + var doc1 = new ManagedDockWindowDocument(window1); + var doc2 = new ManagedDockWindowDocument(window2); + + dock.AddWindow(doc1); + dock.AddWindow(doc2); + + Assert.Equal(2, dock.OpenedDockablesCount); + Assert.Same(doc2, dock.ActiveDockable); + Assert.Same(dock, doc1.Owner); + Assert.Same(factory, doc1.Factory); + Assert.Same(dock, doc2.Owner); + Assert.Same(factory, doc2.Factory); + + dock.RemoveWindow(doc2); + + Assert.Equal(1, dock.OpenedDockablesCount); + Assert.Same(doc1, dock.ActiveDockable); + } + + [AvaloniaFact] + public void ManagedWindowDock_FocusedDockable_Raises_Factory_Event() + { + var factory = new Factory(); + var dock = new ManagedWindowDock { Factory = factory }; + var dockable = new ManagedDockWindowDocument(new DockWindow { Factory = factory }); + + var focused = new List(); + factory.FocusedDockableChanged += (_, e) => + { + if (e.Dockable is { } value) + { + focused.Add(value); + } + }; + + dock.FocusedDockable = dockable; + + Assert.Contains(dockable, focused); + } + + [AvaloniaFact] + public void ManagedWindowRegistry_Register_Unregister_Layer() + { + var factory = new Factory(); + var layer1 = new ManagedWindowLayer { IsVisible = true }; + var layer2 = new ManagedWindowLayer { IsVisible = true }; + + ManagedWindowRegistry.RegisterLayer(factory, layer1); + Assert.NotNull(layer1.Dock); + Assert.Same(ManagedWindowRegistry.GetOrCreateDock(factory), layer1.Dock); + + ManagedWindowRegistry.RegisterLayer(factory, layer2); + Assert.Null(layer1.Dock); + Assert.False(layer1.IsVisible); + Assert.Same(ManagedWindowRegistry.GetOrCreateDock(factory), layer2.Dock); + + ManagedWindowRegistry.UnregisterLayer(factory, layer2); + Assert.Null(layer2.Dock); + Assert.False(layer2.IsVisible); + } + + [AvaloniaFact] + public void ManagedWindowRegistry_TryGetLayer_Returns_Registered_Layer() + { + var factory = new Factory(); + Assert.Null(ManagedWindowRegistry.TryGetLayer(factory)); + + var layer = new ManagedWindowLayer { IsVisible = true }; + ManagedWindowRegistry.RegisterLayer(factory, layer); + + Assert.Same(layer, ManagedWindowRegistry.TryGetLayer(factory)); + + ManagedWindowRegistry.UnregisterLayer(factory, layer); + Assert.Null(ManagedWindowRegistry.TryGetLayer(factory)); + } + + [AvaloniaFact] + public void ManagedWindowLayer_Updates_ZOrder_For_Topmost_Active() + { + var dock = new ManagedWindowDock(); + var window1 = new DockWindow(); + var window2 = new DockWindow { Topmost = true }; + var window3 = new DockWindow { Topmost = true }; + var doc1 = new ManagedDockWindowDocument(window1); + var doc2 = new ManagedDockWindowDocument(window2); + var doc3 = new ManagedDockWindowDocument(window3); + + dock.VisibleDockables!.Add(doc1); + dock.VisibleDockables!.Add(doc2); + dock.VisibleDockables!.Add(doc3); + dock.ActiveDockable = doc2; + + _ = new ManagedWindowLayer { Dock = dock }; + + Assert.True(doc2.MdiZIndex > doc3.MdiZIndex); + Assert.True(doc3.MdiZIndex > doc1.MdiZIndex); + } + + [AvaloniaFact] + public void ManagedWindowLayer_DockableCollection_Changes_Update_Subscriptions() + { + var dockables = new ObservableCollection(); + var dock = new ManagedWindowDock + { + VisibleDockables = dockables + }; + + var layer = new ManagedWindowLayer + { + Dock = dock + }; + + var doc1 = new ManagedDockWindowDocument(new DockWindow()); + var doc2 = new ManagedDockWindowDocument(new DockWindow()); + + dockables.Add(doc1); + var subscriptions = GetWindowSubscriptions(layer); + + Assert.Single(subscriptions); + Assert.Contains(doc1, subscriptions.Keys); + + dockables.Add(doc2); + + Assert.Equal(2, subscriptions.Count); + Assert.Contains(doc2, subscriptions.Keys); + + dockables.Remove(doc1); + + Assert.Single(subscriptions); + Assert.DoesNotContain(doc1, subscriptions.Keys); + + var doc3 = new ManagedDockWindowDocument(new DockWindow()); + dockables[0] = doc3; + + Assert.Single(subscriptions); + Assert.Contains(doc3, subscriptions.Keys); + Assert.DoesNotContain(doc2, subscriptions.Keys); + + dockables.Clear(); + Assert.Empty(subscriptions); + } + + [AvaloniaFact] + public void ManagedWindowLayer_Show_Hide_Overlay() + { + var dock = new ManagedWindowDock(); + var layer = new ManagedWindowLayer + { + Dock = dock, + Width = 300, + Height = 200 + }; + + var window = new Window + { + Width = 400, + Height = 300, + Content = layer + }; + + window.Show(); + try + { + layer.ApplyTemplate(); + window.UpdateLayout(); + + var overlay = new Border(); + layer.ShowOverlay("TestOverlay", overlay, new Rect(10, 20, 120, 80), true); + + var canvas = layer.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.Name == "PART_OverlayCanvas"); + Assert.NotNull(canvas); + Assert.Contains(overlay, canvas!.Children); + Assert.Equal(10, Canvas.GetLeft(overlay)); + Assert.Equal(20, Canvas.GetTop(overlay)); + Assert.Equal(120, overlay.Width); + Assert.Equal(80, overlay.Height); + + layer.HideOverlay("TestOverlay"); + Assert.DoesNotContain(overlay, canvas.Children); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void ManagedWindowLayer_TryGetLayer_Returns_From_DockControl() + { + var factory = new Factory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + + var dockControl = new DockControl + { + Layout = root + }; + + var window = new Window + { + Width = 400, + Height = 300, + Content = dockControl + }; + + window.Show(); + try + { + dockControl.ApplyTemplate(); + window.UpdateLayout(); + + var contentControl = dockControl.GetVisualDescendants() + .OfType() + .FirstOrDefault(control => control.Name == "PART_ContentControl"); + var layer = dockControl.GetVisualDescendants().OfType().FirstOrDefault(); + + Assert.NotNull(contentControl); + Assert.NotNull(layer); + + var resolved = ManagedWindowLayer.TryGetLayer(contentControl!); + + Assert.Same(layer, resolved); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void ManagedWindowLayer_TryGetLayer_Returns_From_VisualRoot() + { + var layer = new ManagedWindowLayer + { + Width = 200, + Height = 100 + }; + + var window = new Window + { + Width = 300, + Height = 200, + Content = layer + }; + + window.Show(); + try + { + window.UpdateLayout(); + var resolved = ManagedWindowLayer.TryGetLayer(layer); + + Assert.Same(layer, resolved); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void ManagedDockableConverters_Resolve_Tool_Window() + { + var factory = new Factory(); + var toolDock = factory.CreateToolDock(); + var documentDock = factory.CreateDocumentDock(); + + var root = factory.CreateRootDock(); + root.Factory = factory; + root.VisibleDockables = factory.CreateList(toolDock, documentDock); + root.ActiveDockable = toolDock; + + var window = new DockWindow + { + Factory = factory, + Layout = root + }; + root.Window = window; + + var managedDocument = new ManagedDockWindowDocument(window); + + var isToolWindow = ManagedDockableIsToolWindowConverter.Instance.Convert( + managedDocument, + typeof(bool), + null, + CultureInfo.InvariantCulture); + Assert.True((bool)isToolWindow); + + var invertedToolWindow = ManagedDockableIsToolWindowConverter.Instance.Convert( + managedDocument, + typeof(bool), + true, + CultureInfo.InvariantCulture); + Assert.False((bool)invertedToolWindow); + + var invertedToolWindowText = ManagedDockableIsToolWindowConverter.Instance.Convert( + managedDocument, + typeof(bool), + "Not", + CultureInfo.InvariantCulture); + Assert.False((bool)invertedToolWindowText); + + var resolvedToolDock = ManagedDockableToolDockConverter.Instance.Convert( + managedDocument, + typeof(IToolDock), + null, + CultureInfo.InvariantCulture); + Assert.Same(toolDock, resolvedToolDock); + + root.ActiveDockable = documentDock; + isToolWindow = ManagedDockableIsToolWindowConverter.Instance.Convert( + managedDocument, + typeof(bool), + null, + CultureInfo.InvariantCulture); + Assert.False((bool)isToolWindow); + + resolvedToolDock = ManagedDockableToolDockConverter.Instance.Convert( + managedDocument, + typeof(IToolDock), + null, + CultureInfo.InvariantCulture); + Assert.Null(resolvedToolDock); + } + + private static Dictionary GetWindowSubscriptions( + ManagedWindowLayer layer) + { + var field = typeof(ManagedWindowLayer).GetField("_windowSubscriptions", BindingFlags.Instance | BindingFlags.NonPublic); + return (Dictionary)field!.GetValue(layer)!; + } + + private sealed class TestHostWindow : IHostWindow + { + private double _x; + private double _y; + private double _width; + private double _height; + + public bool ExitCalled { get; private set; } + + public IHostWindowState? HostWindowState => null; + + public bool IsTracked { get; set; } + + public IDockWindow? Window { get; set; } + + public void Present(bool isDialog) + { + } + + public void Exit() + { + ExitCalled = true; + } + + public void SetPosition(double x, double y) + { + _x = x; + _y = y; + } + + public void GetPosition(out double x, out double y) + { + x = _x; + y = _y; + } + + public void SetSize(double width, double height) + { + _width = width; + _height = height; + } + + public void GetSize(out double width, out double height) + { + width = _width; + height = _height; + } + + public void SetTitle(string? title) + { + } + + public void SetLayout(IDock layout) + { + } + + public void SetActive() + { + } + } + + private sealed class TestDockWindow : DockWindow + { + public override bool OnClose() => true; + } + + private sealed class TestDockManager : IDockManager + { + public DockPoint Position { get; set; } + + public DockPoint ScreenPosition { get; set; } + + public bool PreventSizeConflicts { get; set; } + + public bool ValidateTool(ITool sourceTool, IDockable targetDockable, DragAction action, DockOperation operation, bool bExecute) + { + return true; + } + + public bool ValidateDocument(IDocument sourceDocument, IDockable targetDockable, DragAction action, DockOperation operation, bool bExecute) + { + return true; + } + + public bool ValidateDock(IDock sourceDock, IDockable targetDockable, DragAction action, DockOperation operation, bool bExecute) + { + return true; + } + + public bool ValidateDockable(IDockable sourceDockable, IDockable targetDockable, DragAction action, DockOperation operation, bool bExecute) + { + return true; + } + + public bool IsDockTargetVisible(IDockable sourceDockable, IDockable targetDockable, DockOperation operation) + { + return true; + } + } + + private sealed class TestDockManagerState : DockManagerState + { + public TestDockManagerState(IDockManager dockManager) + : base(dockManager) + { + } + + public void InvokeFloat(Point point, DockControl dockControl, IDockable dockable, IFactory factory, PixelPoint dragOffset) + { + Float(point, dockControl, dockable, factory, dragOffset); + } + + public void InvokeExecute(Point point, DockOperation operation, DragAction dragAction, Visual relativeTo, IDockable sourceDockable, IDockable targetDockable) + { + Execute(point, operation, dragAction, relativeTo, sourceDockable, targetDockable); + } + } +}