Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docfx/articles/dock-controls-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ Unless noted otherwise, the properties listed are Avalonia styled properties and
| `HeaderTemplate` | `IDataTemplate` | Tab header template. |
| `ModifiedTemplate` | `IDataTemplate` | Modified indicator template. |
| `CloseTemplate` | `IDataTemplate` | Close button template. |
| `EmptyContentTemplate` | `IDataTemplate?` | Template used to render model `EmptyContent` when no tabbed documents are visible. |
| `CloseButtonTheme` | `ControlTheme?` | Theme for the close button. |
| `IsActive` | `bool` | Active document state (drives `:active`). |
| `TabsLayout` | `DocumentTabLayout` | Tab placement for the document dock. |
| `HasVisibleDockables` | `bool` | `true` when one or more tabbed dockables are visible (read-only). |

### ToolControl

Expand Down Expand Up @@ -156,9 +158,11 @@ Unless noted otherwise, the properties listed are Avalonia styled properties and
| `HeaderTemplate` | `IDataTemplate` | Window header template. |
| `ModifiedTemplate` | `IDataTemplate` | Modified indicator template. |
| `CloseTemplate` | `IDataTemplate` | Close button template. |
| `EmptyContentTemplate` | `IDataTemplate?` | Template used to render the model `EmptyContent` placeholder when no MDI documents are visible. |
| `CloseButtonTheme` | `ControlTheme?` | Theme for the close button. |
| `LayoutManager` | `IMdiLayoutManager?` | Layout manager forwarded to the MDI panel. |
| `IsActive` | `bool` | Active state (drives `:active`). |
| `HasVisibleDocuments` | `bool` | `true` when one or more MDI documents are visible (read-only). |

### MdiDocumentWindow

Expand Down
20 changes: 20 additions & 0 deletions docfx/articles/dock-mdi.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ The Avalonia controls expose additional properties for theming:
- `MdiDocumentControl` forwards `IconTemplate`, `HeaderTemplate`, `ModifiedTemplate`, `CloseTemplate`, and `CloseButtonTheme` to each window.
- `MdiDocumentWindow` exposes `DocumentContextMenu`, `MdiState`, and `IsActive` for styling or interaction.

`IDocumentDock.EmptyContent` defines placeholder content shown when a document host has no visible dockables. In MDI mode, `MdiDocumentControl.EmptyContentTemplate` can optionally control how that content is rendered:

```csharp
using Avalonia.Controls;
using Avalonia.Controls.Templates;

var documents = new DocumentDock
{
LayoutMode = DocumentLayoutMode.Mdi,
EmptyContent = "No documents are open"
};

var mdiDocumentControl = new MdiDocumentControl
{
DataContext = documents,
EmptyContentTemplate = new FuncDataTemplate<string>((text, _) =>
new TextBlock { Text = text }, true)
};
```

To customize the layout algorithm, provide an `IMdiLayoutManager` implementation. It can be assigned on `MdiDocumentControl.LayoutManager` (which the default template forwards to `MdiLayoutPanel`):

```csharp
Expand Down
4 changes: 4 additions & 0 deletions docfx/articles/dock-model-controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ are placed.
`IDocumentDock.LayoutMode` switches between tabbed documents and classic MDI
windows. When MDI mode is enabled the dock exposes commands for cascade and
tile operations, and documents implement `IMdiDocument` to store window state.
`IDocumentDock.EmptyContent` can provide placeholder content when either
document host is empty (tabbed or MDI). Template rendering for that content is
configured in the Avalonia layer via `DocumentControl.EmptyContentTemplate`
and `MdiDocumentControl.EmptyContentTemplate`.

`IDocumentDockFactory` exposes a `DocumentFactory` delegate that is used by
the `CreateDocument` command. When assigned, this factory is invoked to
Expand Down
5 changes: 3 additions & 2 deletions docfx/articles/dock-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ from a saved state.
- `TabsLayout` chooses where the tabs appear using the `DocumentTabLayout` enum
- `LayoutMode` switches between `Tabbed` and `Mdi` layouts
- `CloseButtonShowMode` controls when document close buttons appear
- `EmptyContent` defines placeholder content shown when a tabbed or MDI document host has no visible dockables
- `CanCreateDocument` and `CreateDocument` control the new-document command
- `CascadeDocuments`, `TileDocumentsHorizontal`, `TileDocumentsVertical`, and `RestoreDocuments` are MDI helpers

Expand Down Expand Up @@ -124,9 +125,9 @@ These properties allow you to customize the context menus, flyouts, and button t

Dock exposes template properties for tab headers and MDI windows:

- `DocumentControl`: `IconTemplate`, `HeaderTemplate`, `ModifiedTemplate`, `CloseTemplate`, `CloseButtonTheme`, `IsActive`, `TabsLayout`.
- `DocumentControl`: `IconTemplate`, `HeaderTemplate`, `ModifiedTemplate`, `CloseTemplate`, `EmptyContentTemplate`, `CloseButtonTheme`, `IsActive`, `TabsLayout`, `HasVisibleDockables`.
- `ToolControl`: `IconTemplate`, `HeaderTemplate`, `ModifiedTemplate`.
- `MdiDocumentControl`: `IconTemplate`, `HeaderTemplate`, `ModifiedTemplate`, `CloseTemplate`, `CloseButtonTheme`, `LayoutManager`, `IsActive`.
- `MdiDocumentControl`: `IconTemplate`, `HeaderTemplate`, `ModifiedTemplate`, `CloseTemplate`, `EmptyContentTemplate`, `CloseButtonTheme`, `LayoutManager`, `IsActive`, `HasVisibleDocuments`.
- `MdiDocumentWindow`: `IconTemplate`, `HeaderTemplate`, `ModifiedTemplate`, `CloseTemplate`, `CloseButtonTheme`, `DocumentContextMenu`, `IsActive`, `MdiState`.

Use these to customize headers, icons, and modified indicators in styles or templates. See [Styling and theming](dock-styling.md) and [MDI document layout](dock-mdi.md) for details.
Expand Down
32 changes: 21 additions & 11 deletions src/Dock.Avalonia.Themes.Fluent/Controls/DocumentControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,17 +118,27 @@
<Grid x:Name="PART_Grid"
IsVisible="{Binding #PART_TabStrip.IsVisible}" />
<Border x:Name="PART_Border">
<DockableControl DataContext="{Binding ActiveDockable}"
TrackingMode="Visible">
<ContentControl x:Name="PART_ContentPresenter"
Content="{Binding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ContentControl.ContentTemplate>
<ControlRecyclingDataTemplate Parent="{Binding #PART_ContentPresenter}" />
</ContentControl.ContentTemplate>
</ContentControl>
</DockableControl>
<Grid>
<DockableControl DataContext="{Binding ActiveDockable}"
TrackingMode="Visible"
IsVisible="{Binding $parent[DocumentControl].HasVisibleDockables}">
<ContentControl x:Name="PART_ContentPresenter"
Content="{Binding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ContentControl.ContentTemplate>
<ControlRecyclingDataTemplate Parent="{Binding #PART_ContentPresenter}" />
</ContentControl.ContentTemplate>
</ContentControl>
</DockableControl>

<ContentControl x:Name="PART_EmptyContentHost"
Content="{Binding EmptyContent}"
ContentTemplate="{Binding $parent[DocumentControl].EmptyContentTemplate}"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
IsVisible="{Binding !$parent[DocumentControl].HasVisibleDockables}" />
</Grid>
</Border>
</DockPanel>
</ControlTemplate>
Expand Down
74 changes: 42 additions & 32 deletions src/Dock.Avalonia.Themes.Fluent/Controls/MdiDocumentControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,38 +68,48 @@
DockProperties.IsDropArea="True"
DockProperties.IsDockTarget="True"
DockProperties.DockAdornerHost="{Binding RelativeSource={RelativeSource Self}}">
<ItemsControl x:Name="PART_ItemsControl"
Classes="MdiLayoutItems"
ItemsSource="{Binding VisibleDockables}">
<ItemsControl.Styles>
<Style Selector="ItemsControl.MdiLayoutItems > ContentPresenter"
x:DataType="dmc:IMdiDocument">
<Setter Property="ZIndex" Value="{Binding MdiZIndex}" />
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:MdiLayoutPanel LayoutManager="{Binding $parent[controls:MdiDocumentControl].LayoutManager}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="dmc:IMdiDocument">
<controls:MdiDocumentWindow IconTemplate="{Binding $parent[controls:MdiDocumentControl].IconTemplate}"
HeaderTemplate="{Binding $parent[controls:MdiDocumentControl].HeaderTemplate}"
ModifiedTemplate="{Binding $parent[controls:MdiDocumentControl].ModifiedTemplate}"
CloseTemplate="{Binding $parent[controls:MdiDocumentControl].CloseTemplate}"
CloseButtonTheme="{Binding $parent[controls:MdiDocumentControl].CloseButtonTheme}"
DocumentContextMenu="{DynamicResource MdiDocumentWindowContextMenu}"
MdiState="{Binding MdiState}"
ZIndex="{Binding MdiZIndex}"
DockProperties.IsDropArea="True"
DockProperties.IsDockTarget="True"
DockProperties.ShowDockIndicatorOnly="True"
DockProperties.IndicatorDockOperation="Fill"
DockProperties.DockAdornerHost="{Binding RelativeSource={RelativeSource AncestorType=controls:MdiDocumentControl}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid>
<ItemsControl x:Name="PART_ItemsControl"
Classes="MdiLayoutItems"
IsVisible="{Binding $parent[controls:MdiDocumentControl].HasVisibleDocuments}"
ItemsSource="{Binding VisibleDockables}">
<ItemsControl.Styles>
<Style Selector="ItemsControl.MdiLayoutItems > ContentPresenter"
x:DataType="dmc:IMdiDocument">
<Setter Property="ZIndex" Value="{Binding MdiZIndex}" />
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:MdiLayoutPanel LayoutManager="{Binding $parent[controls:MdiDocumentControl].LayoutManager}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="dmc:IMdiDocument">
<controls:MdiDocumentWindow IconTemplate="{Binding $parent[controls:MdiDocumentControl].IconTemplate}"
HeaderTemplate="{Binding $parent[controls:MdiDocumentControl].HeaderTemplate}"
ModifiedTemplate="{Binding $parent[controls:MdiDocumentControl].ModifiedTemplate}"
CloseTemplate="{Binding $parent[controls:MdiDocumentControl].CloseTemplate}"
CloseButtonTheme="{Binding $parent[controls:MdiDocumentControl].CloseButtonTheme}"
DocumentContextMenu="{DynamicResource MdiDocumentWindowContextMenu}"
MdiState="{Binding MdiState}"
ZIndex="{Binding MdiZIndex}"
DockProperties.IsDropArea="True"
DockProperties.IsDockTarget="True"
DockProperties.ShowDockIndicatorOnly="True"
DockProperties.IndicatorDockOperation="Fill"
DockProperties.DockAdornerHost="{Binding RelativeSource={RelativeSource AncestorType=controls:MdiDocumentControl}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

<ContentControl x:Name="PART_EmptyContentHost"
Content="{Binding EmptyContent}"
ContentTemplate="{Binding $parent[controls:MdiDocumentControl].EmptyContentTemplate}"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
IsVisible="{Binding !$parent[controls:MdiDocumentControl].HasVisibleDocuments}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
Expand Down
139 changes: 139 additions & 0 deletions src/Dock.Avalonia/Controls/DocumentControl.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// 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.Specialized;
using System.ComponentModel;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Reactive;
using Avalonia.Styling;
using Dock.Model.Core;

Expand Down Expand Up @@ -42,6 +46,12 @@ public class DocumentControl : TemplatedControl
public static readonly StyledProperty<IDataTemplate> CloseTemplateProperty =
AvaloniaProperty.Register<DocumentControl, IDataTemplate>(nameof(CloseTemplate));

/// <summary>
/// Defines the <see cref="EmptyContentTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> EmptyContentTemplateProperty =
AvaloniaProperty.Register<DocumentControl, IDataTemplate?>(nameof(EmptyContentTemplate));

/// <summary>
/// Define the <see cref="CloseButtonTheme"/> property.
/// </summary>
Expand Down Expand Up @@ -69,6 +79,20 @@ public ControlTheme? CloseButtonTheme
public static readonly StyledProperty<DocumentTabLayout> TabsLayoutProperty =
AvaloniaProperty.Register<DocumentControl, DocumentTabLayout>(nameof(TabsLayout));

/// <summary>
/// Defines the <see cref="HasVisibleDockables"/> property.
/// </summary>
public static readonly DirectProperty<DocumentControl, bool> HasVisibleDockablesProperty =
AvaloniaProperty.RegisterDirect<DocumentControl, bool>(
nameof(HasVisibleDockables),
o => o.HasVisibleDockables);

private INotifyPropertyChanged? _dockSubscription;
private INotifyCollectionChanged? _dockablesSubscription;
private IDock? _currentDock;
private IDisposable? _dataContextSubscription;
private bool _hasVisibleDockables;

/// <summary>
/// Gets or sets tab icon template.
/// </summary>
Expand Down Expand Up @@ -105,6 +129,15 @@ public IDataTemplate CloseTemplate
set => SetValue(CloseTemplateProperty, value);
}

/// <summary>
/// Gets or sets template used to render empty host content.
/// </summary>
public IDataTemplate? EmptyContentTemplate
{
get => GetValue(EmptyContentTemplateProperty);
set => SetValue(EmptyContentTemplateProperty, value);
}

/// <summary>
/// Gets or sets if this is the currently active dockable.
/// </summary>
Expand All @@ -123,6 +156,15 @@ public DocumentTabLayout TabsLayout
set => SetValue(TabsLayoutProperty, value);
}

/// <summary>
/// Gets a value indicating whether the current tabbed host contains visible dockables.
/// </summary>
public bool HasVisibleDockables
{
get => _hasVisibleDockables;
private set => SetAndRaise(HasVisibleDockablesProperty, ref _hasVisibleDockables, value);
}

/// <summary>
/// Initializes new instance of the <see cref="DocumentControl"/> class.
/// </summary>
Expand All @@ -137,6 +179,21 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
base.OnAttachedToVisualTree(e);

AddHandler(PointerPressedEvent, PressedHandler, RoutingStrategies.Tunnel);

_dataContextSubscription = this.GetObservable(DataContextProperty)
.Subscribe(new AnonymousObserver<object?>(OnDockChanged));
}

/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);

RemoveHandler(PointerPressedEvent, PressedHandler);

_dataContextSubscription?.Dispose();
_dataContextSubscription = null;
DetachDockSubscriptions();
}

private void PressedHandler(object? sender, PointerPressedEventArgs e)
Expand Down Expand Up @@ -165,4 +222,86 @@ private void UpdatePseudoClasses(bool isActive)
{
PseudoClasses.Set(":active", isActive);
}

private void OnDockChanged(object? dataContext)
{
DetachDockSubscriptions();

_currentDock = dataContext as IDock;
if (_currentDock is null)
{
HasVisibleDockables = false;
return;
}

if (_currentDock is INotifyPropertyChanged propertyChanged)
{
_dockSubscription = propertyChanged;
_dockSubscription.PropertyChanged += DockPropertyChanged;
}

AttachDockablesCollection(_currentDock.VisibleDockables as INotifyCollectionChanged);
UpdateHasVisibleDockables();
}

private void DockPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (_currentDock is null)
{
return;
}

if (e.PropertyName != nameof(IDock.VisibleDockables))
{
return;
}

AttachDockablesCollection(_currentDock.VisibleDockables as INotifyCollectionChanged);
UpdateHasVisibleDockables();
}

private void AttachDockablesCollection(INotifyCollectionChanged? collection)
{
if (_dockablesSubscription != null)
{
_dockablesSubscription.CollectionChanged -= DockablesCollectionChanged;
_dockablesSubscription = null;
}

if (collection is null)
{
return;
}

_dockablesSubscription = collection;
_dockablesSubscription.CollectionChanged += DockablesCollectionChanged;
}

private void DockablesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateHasVisibleDockables();
}

private void UpdateHasVisibleDockables()
{
HasVisibleDockables = _currentDock?.VisibleDockables?.Count > 0;
}

private void DetachDockSubscriptions()
{
if (_dockSubscription != null)
{
_dockSubscription.PropertyChanged -= DockPropertyChanged;
_dockSubscription = null;
}

if (_dockablesSubscription != null)
{
_dockablesSubscription.CollectionChanged -= DockablesCollectionChanged;
_dockablesSubscription = null;
}

_currentDock = null;
HasVisibleDockables = false;
}
}
Loading
Loading