Skip to content

Commit 1bc1360

Browse files
Merge pull request #885 from danwalmsley/feature/offer-global-docking-intelligently
Intelligently offer the global docking options
2 parents 0afe1e8 + 6a04593 commit 1bc1360

File tree

14 files changed

+277
-7
lines changed

14 files changed

+277
-7
lines changed

docs/dock-properties.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ The available properties are:
2020
| `DockAdornerHost` | `Control` | Specifies the element that should display the dock target adorner. |
2121
| `DockGroup` | `string` | Group identifier for restriction-based docking. See [Docking Groups](dock-docking-groups.md). |
2222

23+
## Root Dock Settings
24+
25+
In addition to the properties above, `IRootDock` provides settings that control global docking behavior:
26+
27+
| Property | Type | Default | Purpose |
28+
| -------- | ---- | ------- | ------- |
29+
| `EnableAdaptiveGlobalDockTargets` | `bool` | `false` | When enabled, reduces global dock targets to only show options where the layout would change. Recommended for dashboards and widget areas to simplify the user experience by reducing the number of drop options presented. |
30+
31+
This setting must be configured on your root dock instance and is particularly useful in scenarios where you want to minimize visual clutter and provide a more focused docking experience.
32+
2333
## Using the properties in control themes
2434

2535
Every control template that participates in docking should set the appropriate `DockProperties`. The default themes include them on tab strips, pinned panels and window chrome. When creating a custom template copy these setters so dragging continues to work:

src/Dock.Avalonia.Themes.Fluent/Controls/GlobalDockTarget.axaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,28 @@
124124
<Style Selector="^[ShowIndicatorsOnly=True] /template/ Panel#PART_SelectorPanel">
125125
<Setter Property="IsVisible" Value="False" />
126126
</Style>
127+
128+
<Style Selector="^/template/ Image.selector">
129+
<Setter Property="IsVisible" Value="False" />
130+
</Style>
131+
132+
<Style Selector="^:vertical /template/ Image">
133+
<Style Selector="^#PART_TopSelector.selector">
134+
<Setter Property="IsVisible" Value="True" />
135+
</Style>
136+
<Style Selector="^#PART_BottomSelector.selector">
137+
<Setter Property="IsVisible" Value="True" />
138+
</Style>
139+
</Style>
140+
141+
<Style Selector="^:horizontal /template/ Image">
142+
<Style Selector="^#PART_LeftSelector.selector">
143+
<Setter Property="IsVisible" Value="True" />
144+
</Style>
145+
<Style Selector="^#PART_RightSelector.selector">
146+
<Setter Property="IsVisible" Value="True" />
147+
</Style>
148+
</Style>
127149

128150
</ControlTheme>
129151

src/Dock.Avalonia/Controls/DockTargetBase.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ public abstract class DockTargetBase : TemplatedControl, IDockTarget
5151
/// </summary>
5252
public static readonly StyledProperty<bool> ShowIndicatorsOnlyProperty =
5353
AvaloniaProperty.Register<DockTargetBase, bool>(nameof(ShowIndicatorsOnly));
54+
55+
/// <summary>
56+
/// Defines the <see cref="ShowHorizontalTargets"/> property.
57+
/// </summary>
58+
public static readonly StyledProperty<bool> ShowHorizontalTargetsProperty = AvaloniaProperty.Register<DockTargetBase, bool>(
59+
nameof(ShowHorizontalTargets), defaultValue: true);
60+
61+
62+
/// <summary>
63+
/// Defines the <see cref="ShowVerticalTargets"/> property.
64+
/// </summary>
65+
public static readonly StyledProperty<bool> ShowVerticalTargetsProperty = AvaloniaProperty.Register<DockTargetBase, bool>(
66+
nameof(ShowVerticalTargets), defaultValue: true);
67+
68+
public DockTargetBase()
69+
{
70+
PseudoClasses.Set(":horizontal", this.GetObservable(ShowHorizontalTargetsProperty));
71+
PseudoClasses.Set(":vertical", this.GetObservable(ShowVerticalTargetsProperty));
72+
}
5473

5574
/// <summary>
5675
/// Gets or sets whether only drop indicators should be shown.
@@ -61,6 +80,24 @@ public bool ShowIndicatorsOnly
6180
set => SetValue(ShowIndicatorsOnlyProperty, value);
6281
}
6382

83+
/// <summary>
84+
/// Gets or sets a value indicating whether horizontal docking targets should be displayed.
85+
/// </summary>
86+
public bool ShowHorizontalTargets
87+
{
88+
get => GetValue(ShowHorizontalTargetsProperty);
89+
set => SetValue(ShowHorizontalTargetsProperty, value);
90+
}
91+
92+
/// <summary>
93+
/// Gets or sets whether vertical docking targets should be displayed.
94+
/// </summary>s
95+
public bool ShowVerticalTargets
96+
{
97+
get => GetValue(ShowVerticalTargetsProperty);
98+
set => SetValue(ShowVerticalTargetsProperty, value);
99+
}
100+
64101
/// <summary>
65102
/// A dictionary that maps dock operations to their corresponding indicator controls.
66103
/// </summary>
@@ -346,5 +383,8 @@ void IDockTarget.Reset()
346383
{
347384
control.Opacity = 0;
348385
}
386+
387+
ShowHorizontalTargets = true;
388+
ShowVerticalTargets = true;
349389
}
350390
}

src/Dock.Avalonia/Internal/AdornerHelper.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Wiesław Šoltés. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for details.
3+
4+
using System.Diagnostics;
35
using Avalonia;
46
using Avalonia.Controls;
57
using Avalonia.Controls.Primitives;
@@ -16,19 +18,19 @@ internal class AdornerHelper<T>(bool useFloatingDockAdorner)
1618
private DockAdornerWindow? _window;
1719
private AdornerLayer? _layer;
1820

19-
public void AddAdorner(Visual visual, bool indicatorsOnly)
21+
public void AddAdorner(Visual visual, bool indicatorsOnly, bool allowHorizontalDocking = true, bool allowVerticalDocking = true)
2022
{
2123
if (useFloatingDockAdorner)
2224
{
23-
AddFloatingAdorner(visual, indicatorsOnly);
25+
AddFloatingAdorner(visual, indicatorsOnly, allowHorizontalDocking, allowVerticalDocking);
2426
}
2527
else
2628
{
27-
AddRegularAdorner(visual, indicatorsOnly);
29+
AddRegularAdorner(visual, indicatorsOnly, allowHorizontalDocking, allowVerticalDocking);
2830
}
2931
}
3032

31-
private void AddFloatingAdorner(Visual visual, bool indicatorsOnly)
33+
private void AddFloatingAdorner(Visual visual, bool indicatorsOnly, bool horizontalDocking, bool verticalDocking)
3234
{
3335
if (_window is not null)
3436
{
@@ -52,6 +54,8 @@ private void AddFloatingAdorner(Visual visual, bool indicatorsOnly)
5254
else if (adorner is GlobalDockTarget globalDockTarget)
5355
{
5456
globalDockTarget.ShowIndicatorsOnly = indicatorsOnly;
57+
globalDockTarget.ShowHorizontalTargets = horizontalDocking;
58+
globalDockTarget.ShowVerticalTargets = verticalDocking;
5559
}
5660
}
5761

@@ -83,7 +87,7 @@ private void AddFloatingAdorner(Visual visual, bool indicatorsOnly)
8387
_window.Show(root);
8488
}
8589

86-
private void AddRegularAdorner(Visual visual, bool indicatorsOnly)
90+
private void AddRegularAdorner(Visual visual, bool indicatorsOnly, bool horizontalDocking, bool verticalDocking)
8791
{
8892
if (_window is not null)
8993
{
@@ -108,6 +112,8 @@ private void AddRegularAdorner(Visual visual, bool indicatorsOnly)
108112
break;
109113
case GlobalDockTarget globalDockTarget:
110114
globalDockTarget.ShowIndicatorsOnly = indicatorsOnly;
115+
globalDockTarget.ShowHorizontalTargets = horizontalDocking;
116+
globalDockTarget.ShowVerticalTargets = verticalDocking;
111117
break;
112118
}
113119
}

src/Dock.Avalonia/Internal/DockManagerState.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,25 @@ protected void AddAdorners(bool isLocalValid, bool isGlobalValid)
4242
{
4343
var host = DockProperties.GetDockAdornerHost(control) ?? control;
4444
var indicatorsOnly = DockProperties.GetShowDockIndicatorOnly(control);
45-
LocalAdornerHelper.AddAdorner(host, indicatorsOnly);
45+
LocalAdornerHelper.AddAdorner(host, indicatorsOnly, true, true);
4646
}
4747

4848
// Global dock target
4949
if (isGlobalValid && DropControl is { } dropControl)
5050
{
51+
bool horizontalGlobalDocking = true;
52+
bool verticalGlobalDocking = true;
53+
54+
if (DropControl.DataContext is IDockable { Factory: { } factory } dockable)
55+
{
56+
var root = factory.FindRoot(dockable);
57+
58+
if (root is { EnableAdaptiveGlobalDockTargets: true })
59+
{
60+
(horizontalGlobalDocking, verticalGlobalDocking) = GlobalDockingHelper.CanGlobalDock(root);
61+
}
62+
}
63+
5164
// Try to find DockControl ancestor - look through the visual tree more thoroughly
5265
var dockControl = dropControl.FindAncestorOfType<DockControl>();
5366

@@ -73,7 +86,7 @@ protected void AddAdorners(bool isLocalValid, bool isGlobalValid)
7386
if (DockInheritanceHelper.GetEffectiveEnableGlobalDocking(targetDock))
7487
{
7588
var indicatorsOnly = DockProperties.GetShowDockIndicatorOnly(dropControl);
76-
GlobalAdornerHelper.AddAdorner(dockControl, indicatorsOnly);
89+
GlobalAdornerHelper.AddAdorner(dockControl, indicatorsOnly, horizontalGlobalDocking, verticalGlobalDocking);
7790
}
7891
}
7992
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Wiesław Šoltés. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for details.
3+
using Dock.Model.Controls;
4+
using Dock.Model.Core;
5+
6+
namespace Dock.Avalonia.Internal;
7+
8+
/// <summary>
9+
/// Helper class for determining when global docking operations make sense
10+
/// </summary>
11+
internal static class GlobalDockingHelper
12+
{
13+
/// <summary>
14+
/// Determines if global docking should be available for the specified target dock
15+
/// </summary>
16+
/// <param name="targetDock">The target dock to evaluate</param>
17+
/// <returns>A tuple indicating whether global docking makes sense horizontally and vertically</returns>
18+
public static (bool horizontalValid, bool verticalValid) CanGlobalDock(IRootDock targetDock)
19+
{
20+
int horizontalCount = 0;
21+
int verticalCount = 0;
22+
23+
CountProportionalDockChildren(targetDock, ref horizontalCount, ref verticalCount);
24+
25+
return (verticalCount >= 2, horizontalCount >= 2);
26+
}
27+
28+
/// <summary>
29+
/// Recursively counts children in horizontal and vertical proportional docks
30+
/// </summary>
31+
/// <param name="dock">The dock to analyze</param>
32+
/// <param name="horizontalCount">Count of items in horizontal proportional docks</param>
33+
/// <param name="verticalCount">Count of items in vertical proportional docks</param>
34+
private static void CountProportionalDockChildren(IDock dock, ref int horizontalCount, ref int verticalCount)
35+
{
36+
// Early exit if we already have enough counts
37+
if (horizontalCount >= 2 && verticalCount >= 2)
38+
{
39+
return;
40+
}
41+
42+
if (dock is IProportionalDock proportionalDock && proportionalDock.VisibleDockables != null)
43+
{
44+
// Count only non-proportional dock children
45+
int nonProportionalChildren = 0;
46+
foreach (var dockable in proportionalDock.VisibleDockables)
47+
{
48+
if (dockable is not IProportionalDock { VisibleDockables: { Count: 1} } && dockable is not ISplitter)
49+
{
50+
nonProportionalChildren++;
51+
}
52+
}
53+
54+
// Add the count to the appropriate orientation
55+
if (proportionalDock.Orientation == Orientation.Horizontal)
56+
{
57+
horizontalCount += nonProportionalChildren;
58+
}
59+
else // Vertical
60+
{
61+
verticalCount += nonProportionalChildren;
62+
}
63+
64+
// Early exit if we've reached the threshold
65+
if (horizontalCount >= 2 && verticalCount >= 2)
66+
{
67+
return;
68+
}
69+
70+
// Recursively check child proportional docks only
71+
foreach (var dockable in proportionalDock.VisibleDockables)
72+
{
73+
if (dockable is IProportionalDock childProportionalDock)
74+
{
75+
CountProportionalDockChildren(childProportionalDock, ref horizontalCount, ref verticalCount);
76+
77+
// Early exit if we've reached the threshold
78+
if (horizontalCount >= 2 && verticalCount >= 2)
79+
{
80+
return;
81+
}
82+
}
83+
}
84+
}
85+
else if (dock.VisibleDockables != null)
86+
{
87+
// For non-proportional docks, recursively check child proportional docks only
88+
foreach (var dockable in dock.VisibleDockables)
89+
{
90+
if (dockable is IProportionalDock childProportionalDock)
91+
{
92+
CountProportionalDockChildren(childProportionalDock, ref horizontalCount, ref verticalCount);
93+
94+
// Early exit if we've reached the threshold
95+
if (horizontalCount >= 2 && verticalCount >= 2)
96+
{
97+
return;
98+
}
99+
}
100+
}
101+
}
102+
}
103+
}

src/Dock.Model.Avalonia/Controls/RootDock.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ public class RootDock : DockBase, IRootDock
9797
o => o.Windows,
9898
(o, v) => o.Windows = v);
9999

100+
public static readonly DirectProperty<RootDock, bool> EnableAdaptiveGlobalDockTargetsProperty =
101+
AvaloniaProperty.RegisterDirect<RootDock, bool>(
102+
nameof(EnableAdaptiveGlobalDockTargets), o => o.EnableAdaptiveGlobalDockTargets,
103+
(o, v) => o.EnableAdaptiveGlobalDockTargets = v);
104+
100105
private bool _isFocusableRoot;
101106
private IList<IDockable>? _hiddenDockables;
102107
private IList<IDockable>? _leftPinnedDockables;
@@ -106,6 +111,7 @@ public class RootDock : DockBase, IRootDock
106111
private IToolDock? _pinnedDock;
107112
private IDockWindow? _window;
108113
private IList<IDockWindow>? _windows;
114+
private bool _enableAdaptiveGlobalDockTargets;
109115

110116
/// <summary>
111117
/// Initializes new instance of the <see cref="RootDock"/> class.
@@ -214,4 +220,13 @@ public IList<IDockWindow>? Windows
214220
[IgnoreDataMember]
215221
[JsonIgnore]
216222
public ICommand ExitWindows { get; }
223+
224+
/// <inheritdoc/>
225+
[DataMember(IsRequired = false, EmitDefaultValue = true)]
226+
[JsonPropertyName("EnableAdaptiveGlobalDockTargets")]
227+
public bool EnableAdaptiveGlobalDockTargets
228+
{
229+
get => _enableAdaptiveGlobalDockTargets;
230+
set => SetAndRaise(EnableAdaptiveGlobalDockTargetsProperty, ref _enableAdaptiveGlobalDockTargets, value);
231+
}
217232
}

src/Dock.Model.Inpc/Controls/RootDock.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class RootDock : DockBase, IRootDock
2424
private IDockWindow? _window;
2525
private IList<IDockWindow>? _windows;
2626
private IToolDock? _pinnedDock;
27+
private bool _enableAdaptiveGlobalDockTargets;
2728

2829
/// <summary>
2930
/// Initializes new instance of the <see cref="RootDock"/> class.
@@ -113,4 +114,12 @@ public IList<IDockWindow>? Windows
113114
/// <inheritdoc/>
114115
[IgnoreDataMember]
115116
public ICommand ExitWindows { get; }
117+
118+
/// <inheritdoc/>
119+
[DataMember(IsRequired = false, EmitDefaultValue = true)]
120+
public bool EnableAdaptiveGlobalDockTargets
121+
{
122+
get => _enableAdaptiveGlobalDockTargets;
123+
set => SetProperty(ref _enableAdaptiveGlobalDockTargets, value);
124+
}
116125
}

src/Dock.Model.Mvvm/Controls/RootDock.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class RootDock : DockBase, IRootDock
2525
private IDockWindow? _window;
2626
private IList<IDockWindow>? _windows;
2727
private IToolDock? _pinnedDock;
28+
private bool _enableAdaptiveGlobalDockTargets;
2829

2930
/// <summary>
3031
/// Initializes new instance of the <see cref="RootDock"/> class.
@@ -114,4 +115,12 @@ public IList<IDockWindow>? Windows
114115
/// <inheritdoc/>
115116
[IgnoreDataMember]
116117
public ICommand ExitWindows { get; }
118+
119+
/// <inheritdoc/>
120+
[DataMember(IsRequired = false, EmitDefaultValue = true)]
121+
public bool EnableAdaptiveGlobalDockTargets
122+
{
123+
get => _enableAdaptiveGlobalDockTargets;
124+
set => SetProperty(ref _enableAdaptiveGlobalDockTargets, value);
125+
}
117126
}

0 commit comments

Comments
 (0)